linguabot commited on
Commit
e9db41c
·
verified ·
1 Parent(s): a584510

Upload folder using huggingface_hub

Browse files
client/src/components/Refinity.tsx CHANGED
@@ -53,8 +53,10 @@ type Task = {
53
 
54
  // ---- Filename helpers ----
55
  function toSafeName(input: string): string {
 
56
  return String(input || '')
57
- .replace(/[^\w\-\s]/g, '')
 
58
  .trim()
59
  .replace(/\s+/g, '_');
60
  }
@@ -70,6 +72,30 @@ function yyyymmdd_hhmm(d = new Date()): string {
70
  return `${Y}${M}${D}_${h}${m}`;
71
  }
72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  const mockTasks: Task[] = [
74
  { id: 't1', title: 'Refinity Demo Task 1', sourceText: 'The quick brown fox jumps over the lazy dog.' },
75
  { id: 't2', title: 'Refinity Demo Task 2', sourceText: 'To be, or not to be, that is the question.' },
@@ -1249,11 +1275,17 @@ const Refinity: React.FC = () => {
1249
  if (!a || !b || a.id===b.id) return;
1250
  try {
1251
  const base = ((api.defaults as any)?.baseURL as string || '').replace(/\/$/, '');
1252
- const latestReviser = (b.revisedBy || b.originalAuthor || username || 'User');
1253
- const A = a.versionNumber, B = b.versionNumber;
 
 
1254
  const filename = `${toSafeName(task?.title||'Task')}_Compare-v${A}-v${B}_Inline_${yyyymmdd_hhmm()}_${toSafeName(latestReviser)}.docx`;
1255
- const body = { prev: a.content||'', current: b.content||'', filename, authorName: latestReviser };
1256
- const resp = await fetch(`${base}/api/refinity/track-changes-comments`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
 
 
 
 
1257
  if (!resp.ok) throw new Error('Export failed');
1258
  const blob = await resp.blob();
1259
  const url = window.URL.createObjectURL(blob);
@@ -1268,18 +1300,27 @@ const Refinity: React.FC = () => {
1268
  if (!a || !b || a.id===b.id) return;
1269
  try {
1270
  const base = ((api.defaults as any)?.baseURL as string || '').replace(/\/$/, '');
1271
- const latestReviser = (b.revisedBy || b.originalAuthor || username || 'User');
1272
- const A = a.versionNumber, B = b.versionNumber;
 
 
1273
  const filename = `${toSafeName(task?.title||'Task')}_Compare-v${A}-v${B}_Comments_${yyyymmdd_hhmm()}_${toSafeName(latestReviser)}.docx`;
1274
- const body = { prev: a.content||'', current: b.content||'', filename, authorName: latestReviser };
1275
- const resp = await fetch(`${base}/api/refinity/track-changes-ooxml`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
 
 
 
 
 
 
 
1276
  if (!resp.ok) throw new Error('Export failed');
1277
  const blob = await resp.blob();
1278
  const url = window.URL.createObjectURL(blob);
1279
  const link = document.createElement('a');
1280
  link.href = url; link.download = filename; document.body.appendChild(link); link.click(); link.remove(); window.URL.revokeObjectURL(url);
1281
  } catch {}
1282
- }} className="block w-full text-left px-3 py-2 text-sm hover:bg-gray-50">Side Comments</button>
1283
  </div>, document.body
1284
  )}
1285
  </div>
@@ -1332,11 +1373,13 @@ const Refinity: React.FC = () => {
1332
  const b = taskVersions.find(v=>v.id===lastDiffIds.b || v.id===compareB);
1333
  if (!a || !b || a.id===b.id) return;
1334
  const base = ((api.defaults as any)?.baseURL as string || '').replace(/\/$/, '');
1335
- const latestReviser = (b?.revisedBy || b?.originalAuthor || username || 'User');
1336
- const A = a.versionNumber, B = b.versionNumber;
 
 
1337
  const filename = `${toSafeName(task?.title||'Task')}_Compare-v${A}-v${B}_Inline_${yyyymmdd_hhmm()}_${toSafeName(latestReviser)}.docx`;
1338
- const body = { prev: (a.content||''), current: (b.content||''), filename, authorName: latestReviser };
1339
- const resp = await fetch(`${base}/api/refinity/track-changes-comments`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
1340
  if (!resp.ok) throw new Error('Export failed');
1341
  const blob = await resp.blob();
1342
  const url = window.URL.createObjectURL(blob);
@@ -1350,10 +1393,12 @@ const Refinity: React.FC = () => {
1350
  const b = taskVersions.find(v=>v.id===lastDiffIds.b || v.id===compareB);
1351
  if (!a || !b || a.id===b.id) return;
1352
  const base = ((api.defaults as any)?.baseURL as string || '').replace(/\/$/, '');
1353
- const latestReviser = (b?.revisedBy || b?.originalAuthor || username || 'User');
1354
- const A = a.versionNumber, B = b.versionNumber;
 
 
1355
  const filename = `${toSafeName(task?.title||'Task')}_Compare-v${A}-v${B}_Comments_${yyyymmdd_hhmm()}_${toSafeName(latestReviser)}.docx`;
1356
- const body = { prev: (a.content||''), current: (b.content||''), filename, authorName: latestReviser };
1357
  const resp = await fetch(`${base}/api/refinity/track-changes-ooxml`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
1358
  if (!resp.ok) throw new Error('Export failed');
1359
  const blob = await resp.blob();
@@ -1361,11 +1406,11 @@ const Refinity: React.FC = () => {
1361
  const link = document.createElement('a');
1362
  link.href = url; link.download = filename; document.body.appendChild(link); link.click(); link.remove(); window.URL.revokeObjectURL(url);
1363
  } catch {}
1364
- }} className="px-3 py-1.5 text-sm rounded-md border border-gray-300 bg-white">Export (Side Comments)</button>
1365
  <button onClick={()=>setCompareModalOpen(false)} className="px-3 py-1.5 text-sm rounded-md border border-gray-300 bg-white">Close</button>
1366
  </div>
1367
  </div>
1368
- <div className="prose prose-sm max-w-none text-gray-900" dangerouslySetInnerHTML={{ __html: compareDiffHtml }} />
1369
  </div>
1370
  </div>
1371
  )}
@@ -1626,7 +1671,10 @@ const EditorPane: React.FC<{ source: string; initialTranslation: string; onBack:
1626
  const resp = await fetch(`${base}/api/refinity/diff`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prev: initialTranslation || '', current: text || '' }) });
1627
  const data = await resp.json().catch(()=>({}));
1628
  if (!resp.ok) throw new Error(data?.error || 'Diff failed');
1629
- setDiffHtml(String(data?.html || ''));
 
 
 
1630
  setShowDiff(true);
1631
  } catch {}
1632
  };
@@ -1638,7 +1686,7 @@ const EditorPane: React.FC<{ source: string; initialTranslation: string; onBack:
1638
  const userNameSafe = toSafeName(username || 'User');
1639
  const vNum = `v${nextVersionNumber}`;
1640
  const filename = `${taskNameSafe}_${vNum}_Clean_${yyyymmdd_hhmm()}_${userNameSafe}.docx`;
1641
- const resp = await fetch(`${base}/api/refinity/track-changes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prev: initialTranslation || '', current: text || '', filename }) });
1642
  if (!resp.ok) throw new Error('Export failed');
1643
  const blob = await resp.blob();
1644
  const url = window.URL.createObjectURL(blob);
@@ -1763,8 +1811,8 @@ const EditorPane: React.FC<{ source: string; initialTranslation: string; onBack:
1763
  </button>
1764
  {revDownloadOpen && revMenuPos && createPortal(
1765
  <div style={{ position: 'fixed', left: revMenuPos.left, top: revMenuPos.top, zIndex: 10000, maxHeight: '240px', overflowY: 'auto' }} className="w-56 rounded-md border border-gray-200 bg-white shadow-lg text-left">
1766
- <button onClick={async()=>{ setRevDownloadOpen(false); try { const base=((api.defaults as any)?.baseURL as string||'').replace(/\/$/,''); const filename=`${toSafeName(taskTitle||'Task')}_${yyyymmdd_hhmm()}_${toSafeName(username||'User')}.docx`; const body={ current: text||'', filename }; const resp=await fetch(`${base}/api/refinity/export-plain`,{ method:'POST', headers:{ 'Content-Type':'application/json' }, body: JSON.stringify(body) }); if(!resp.ok) throw new Error('Export failed'); const blob=await resp.blob(); const url=window.URL.createObjectURL(blob); const link=document.createElement('a'); link.href=url; link.download=filename; document.body.appendChild(link); link.click(); link.remove(); window.URL.revokeObjectURL(url);} catch {} }} className="block w-full text-left px-3 py-2 text-sm hover:bg-gray-50">Without Annotations</button>
1767
- <button onClick={async()=>{ setRevDownloadOpen(false); try { const base=((api.defaults as any)?.baseURL as string||'').replace(/\/$/,''); const filename=`${toSafeName(taskTitle||'Task')}_v${nextVersionNumber}_Annotated_${yyyymmdd_hhmm()}_${toSafeName(username||'User')}.docx`; const list=versionAnnotations.map(a=>({ start:a.start, end:a.end, category:a.category, comment:a.comment })); const body={ current: text||'', filename, annotations: list }; const resp=await fetch(`${base}/api/refinity/export-plain-with-annotations`,{ method:'POST', headers:{ 'Content-Type':'application/json' }, body: JSON.stringify(body) }); if(!resp.ok) throw new Error('Export failed'); const blob=await resp.blob(); const url=window.URL.createObjectURL(blob); const link=document.createElement('a'); link.href=url; link.download=filename; document.body.appendChild(link); link.click(); link.remove(); window.URL.revokeObjectURL(url);} catch {} }} className="block w-full text-left px-3 py-2 text-sm hover:bg-gray-50">With Annotations</button>
1768
  </div>, document.body
1769
  )}
1770
  </div>
@@ -1778,7 +1826,7 @@ const EditorPane: React.FC<{ source: string; initialTranslation: string; onBack:
1778
  <div className="relative rounded-xl bg-white/10 backdrop-blur-md ring-1 ring-inset ring-white/30 shadow-[inset_0_0.5px_0_rgba(255,255,255,0.5),inset_0_-1px_1.5px_rgba(0,0,0,0.12)] p-4">
1779
  <div className="pointer-events-none absolute inset-0 rounded-xl opacity-50 [background:linear-gradient(to_bottom,rgba(255,255,255,0.3),rgba(255,255,255,0)_28%),linear-gradient(to_right,rgba(255,255,255,0.28),rgba(255,255,255,0)_28%)]" />
1780
  <div className="pointer-events-none absolute inset-0 rounded-xl bg-gradient-to-tr from-white/30 via-white/10 to-transparent opacity-45" />
1781
- <div className="relative text-gray-900 prose prose-sm max-w-none" dangerouslySetInnerHTML={{ __html: diffHtml }} />
1782
  </div>
1783
  </div>
1784
  )}
 
53
 
54
  // ---- Filename helpers ----
55
  function toSafeName(input: string): string {
56
+ // Keep CJK and most unicode letters; remove only forbidden filename characters and collapse whitespace
57
  return String(input || '')
58
+ .replace(/[\\/:*?"<>|]+/g, '_') // forbidden on Windows/macOS
59
+ .replace(/[\u0000-\u001F\u007F]/g, '') // control chars
60
  .trim()
61
  .replace(/\s+/g, '_');
62
  }
 
72
  return `${Y}${M}${D}_${h}${m}`;
73
  }
74
 
75
+ // Download helpers with fail-safes
76
+ async function saveBlobToDisk(blob: Blob, filename: string) {
77
+ const url = window.URL.createObjectURL(blob);
78
+ const link = document.createElement('a');
79
+ link.href = url;
80
+ link.download = filename;
81
+ document.body.appendChild(link);
82
+ link.click();
83
+ link.remove();
84
+ window.URL.revokeObjectURL(url);
85
+ }
86
+ function saveTextFallback(text: string, filename: string) {
87
+ const blob = new Blob([text || ''], { type: 'text/plain;charset=utf-8' });
88
+ const fallbackName = filename.replace(/\.docx$/i, '') + '.txt';
89
+ return saveBlobToDisk(blob, fallbackName);
90
+ }
91
+ async function tryFetchDocx(url: string, body: any): Promise<Response> {
92
+ return await fetch(url, {
93
+ method: 'POST',
94
+ headers: { 'Content-Type': 'application/json' },
95
+ body: JSON.stringify(body)
96
+ });
97
+ }
98
+
99
  const mockTasks: Task[] = [
100
  { id: 't1', title: 'Refinity Demo Task 1', sourceText: 'The quick brown fox jumps over the lazy dog.' },
101
  { id: 't2', title: 'Refinity Demo Task 2', sourceText: 'To be, or not to be, that is the question.' },
 
1275
  if (!a || !b || a.id===b.id) return;
1276
  try {
1277
  const base = ((api.defaults as any)?.baseURL as string || '').replace(/\/$/, '');
1278
+ const older = (a.versionNumber <= b.versionNumber) ? a : b;
1279
+ const newer = (a.versionNumber <= b.versionNumber) ? b : a;
1280
+ const latestReviser = (newer.revisedBy || newer.originalAuthor || username || 'User');
1281
+ const A = older.versionNumber, B = newer.versionNumber;
1282
  const filename = `${toSafeName(task?.title||'Task')}_Compare-v${A}-v${B}_Inline_${yyyymmdd_hhmm()}_${toSafeName(latestReviser)}.docx`;
1283
+ const body = { prev: older.content||'', current: newer.content||'', filename, authorName: latestReviser };
1284
+ let resp = await fetch(`${base}/api/refinity/track-changes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
1285
+ if (!resp.ok) {
1286
+ // Fallback to plain export of current text
1287
+ resp = await fetch(`${base}/api/refinity/export-plain`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ current: b.content||'', filename }) });
1288
+ }
1289
  if (!resp.ok) throw new Error('Export failed');
1290
  const blob = await resp.blob();
1291
  const url = window.URL.createObjectURL(blob);
 
1300
  if (!a || !b || a.id===b.id) return;
1301
  try {
1302
  const base = ((api.defaults as any)?.baseURL as string || '').replace(/\/$/, '');
1303
+ const older = (a.versionNumber <= b.versionNumber) ? a : b;
1304
+ const newer = (a.versionNumber <= b.versionNumber) ? b : a;
1305
+ const latestReviser = (newer.revisedBy || newer.originalAuthor || username || 'User');
1306
+ const A = older.versionNumber, B = newer.versionNumber;
1307
  const filename = `${toSafeName(task?.title||'Task')}_Compare-v${A}-v${B}_Comments_${yyyymmdd_hhmm()}_${toSafeName(latestReviser)}.docx`;
1308
+ const body = { prev: older.content||'', current: newer.content||'', filename, authorName: latestReviser, includeComments: false };
1309
+ let resp = await fetch(`${base}/api/refinity/track-changes-ooxml`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
1310
+ if (!resp.ok) {
1311
+ // Fallback to inline; then to plain as last resort
1312
+ resp = await fetch(`${base}/api/refinity/track-changes-ooxml`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...body, includeComments: false }) });
1313
+ if (!resp.ok) {
1314
+ resp = await fetch(`${base}/api/refinity/export-plain`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ current: b.content||'', filename }) });
1315
+ }
1316
+ }
1317
  if (!resp.ok) throw new Error('Export failed');
1318
  const blob = await resp.blob();
1319
  const url = window.URL.createObjectURL(blob);
1320
  const link = document.createElement('a');
1321
  link.href = url; link.download = filename; document.body.appendChild(link); link.click(); link.remove(); window.URL.revokeObjectURL(url);
1322
  } catch {}
1323
+ }} className="block w-full text-left px-3 py-2 text-sm hover:bg-gray-50">Tracked Changes</button>
1324
  </div>, document.body
1325
  )}
1326
  </div>
 
1373
  const b = taskVersions.find(v=>v.id===lastDiffIds.b || v.id===compareB);
1374
  if (!a || !b || a.id===b.id) return;
1375
  const base = ((api.defaults as any)?.baseURL as string || '').replace(/\/$/, '');
1376
+ const older = (a.versionNumber <= b.versionNumber) ? a : b;
1377
+ const newer = (a.versionNumber <= b.versionNumber) ? b : a;
1378
+ const latestReviser = (newer?.revisedBy || newer?.originalAuthor || username || 'User');
1379
+ const A = older.versionNumber, B = newer.versionNumber;
1380
  const filename = `${toSafeName(task?.title||'Task')}_Compare-v${A}-v${B}_Inline_${yyyymmdd_hhmm()}_${toSafeName(latestReviser)}.docx`;
1381
+ const body = { prev: (older.content||''), current: (newer.content||''), filename, authorName: latestReviser };
1382
+ const resp = await fetch(`${base}/api/refinity/track-changes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
1383
  if (!resp.ok) throw new Error('Export failed');
1384
  const blob = await resp.blob();
1385
  const url = window.URL.createObjectURL(blob);
 
1393
  const b = taskVersions.find(v=>v.id===lastDiffIds.b || v.id===compareB);
1394
  if (!a || !b || a.id===b.id) return;
1395
  const base = ((api.defaults as any)?.baseURL as string || '').replace(/\/$/, '');
1396
+ const older = (a.versionNumber <= b.versionNumber) ? a : b;
1397
+ const newer = (a.versionNumber <= b.versionNumber) ? b : a;
1398
+ const latestReviser = (newer?.revisedBy || newer?.originalAuthor || username || 'User');
1399
+ const A = older.versionNumber, B = newer.versionNumber;
1400
  const filename = `${toSafeName(task?.title||'Task')}_Compare-v${A}-v${B}_Comments_${yyyymmdd_hhmm()}_${toSafeName(latestReviser)}.docx`;
1401
+ const body = { prev: (older.content||''), current: (newer.content||''), filename, authorName: latestReviser, includeComments: false };
1402
  const resp = await fetch(`${base}/api/refinity/track-changes-ooxml`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
1403
  if (!resp.ok) throw new Error('Export failed');
1404
  const blob = await resp.blob();
 
1406
  const link = document.createElement('a');
1407
  link.href = url; link.download = filename; document.body.appendChild(link); link.click(); link.remove(); window.URL.revokeObjectURL(url);
1408
  } catch {}
1409
+ }} className="px-3 py-1.5 text-sm rounded-md border border-gray-300 bg-white">Export (Tracked Changes)</button>
1410
  <button onClick={()=>setCompareModalOpen(false)} className="px-3 py-1.5 text-sm rounded-md border border-gray-300 bg-white">Close</button>
1411
  </div>
1412
  </div>
1413
+ <div className="prose prose-sm max-w-none text-gray-900 whitespace-pre-wrap" dangerouslySetInnerHTML={{ __html: compareDiffHtml }} />
1414
  </div>
1415
  </div>
1416
  )}
 
1671
  const resp = await fetch(`${base}/api/refinity/diff`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prev: initialTranslation || '', current: text || '' }) });
1672
  const data = await resp.json().catch(()=>({}));
1673
  if (!resp.ok) throw new Error(data?.error || 'Diff failed');
1674
+ // Ensure layout preserved if backend HTML lacks <br/>
1675
+ const raw = String(data?.html || '');
1676
+ const ensured = raw.includes('<br') ? raw : raw.replace(/\n/g, '<br/>');
1677
+ setDiffHtml(ensured);
1678
  setShowDiff(true);
1679
  } catch {}
1680
  };
 
1686
  const userNameSafe = toSafeName(username || 'User');
1687
  const vNum = `v${nextVersionNumber}`;
1688
  const filename = `${taskNameSafe}_${vNum}_Clean_${yyyymmdd_hhmm()}_${userNameSafe}.docx`;
1689
+ const resp = await fetch(`${base}/api/refinity/track-changes-ooxml`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prev: initialTranslation || '', current: text || '', filename, includeComments: false }) });
1690
  if (!resp.ok) throw new Error('Export failed');
1691
  const blob = await resp.blob();
1692
  const url = window.URL.createObjectURL(blob);
 
1811
  </button>
1812
  {revDownloadOpen && revMenuPos && createPortal(
1813
  <div style={{ position: 'fixed', left: revMenuPos.left, top: revMenuPos.top, zIndex: 10000, maxHeight: '240px', overflowY: 'auto' }} className="w-56 rounded-md border border-gray-200 bg-white shadow-lg text-left">
1814
+ <button onClick={async()=>{ setRevDownloadOpen(false); try { const base=((api.defaults as any)?.baseURL as string||'').replace(/\/$/,''); const filename=`${toSafeName(taskTitle||'Task')}_${yyyymmdd_hhmm()}_${toSafeName(username||'User')}.docx`; const body={ current: text||'', filename }; let resp=await fetch(`${base}/api/refinity/export-plain`,{ method:'POST', headers:{ 'Content-Type':'application/json' }, body: JSON.stringify(body) }); if(!resp.ok) throw new Error('Export failed'); const blob=await resp.blob(); await saveBlobToDisk(blob, filename);} catch { await saveTextFallback(text||'', `${toSafeName(taskTitle||'Task')}_${yyyymmdd_hhmm()}_${toSafeName(username||'User')}.txt`);} }} className="block w-full text-left px-3 py-2 text-sm hover:bg-gray-50">Without Annotations</button>
1815
+ <button onClick={async()=>{ setRevDownloadOpen(false); try { const base=((api.defaults as any)?.baseURL as string||'').replace(/\/$/,''); const filename=`${toSafeName(taskTitle||'Task')}_v${nextVersionNumber}_Annotated_${yyyymmdd_hhmm()}_${toSafeName(username||'User')}.docx`; const list=versionAnnotations.map(a=>({ start:a.start, end:a.end, category:a.category, comment:a.comment })); const body={ current: text||'', filename, annotations: list }; let resp=await fetch(`${base}/api/refinity/export-plain-with-annotations`,{ method:'POST', headers:{ 'Content-Type':'application/json' }, body: JSON.stringify(body) }); if(!resp.ok){ resp=await fetch(`${base}/api/refinity/export-plain`,{ method:'POST', headers:{ 'Content-Type':'application/json' }, body: JSON.stringify({ current: text||'', filename }) }); } if(!resp.ok) throw new Error('Export failed'); const blob=await resp.blob(); await saveBlobToDisk(blob, filename);} catch { await saveTextFallback(text||'', `${toSafeName(taskTitle||'Task')}_${yyyymmdd_hhmm()}_${toSafeName(username||'User')}.txt`);} }} className="block w-full text-left px-3 py-2 text-sm hover:bg-gray-50">With Annotations</button>
1816
  </div>, document.body
1817
  )}
1818
  </div>
 
1826
  <div className="relative rounded-xl bg-white/10 backdrop-blur-md ring-1 ring-inset ring-white/30 shadow-[inset_0_0.5px_0_rgba(255,255,255,0.5),inset_0_-1px_1.5px_rgba(0,0,0,0.12)] p-4">
1827
  <div className="pointer-events-none absolute inset-0 rounded-xl opacity-50 [background:linear-gradient(to_bottom,rgba(255,255,255,0.3),rgba(255,255,255,0)_28%),linear-gradient(to_right,rgba(255,255,255,0.28),rgba(255,255,255,0)_28%)]" />
1828
  <div className="pointer-events-none absolute inset-0 rounded-xl bg-gradient-to-tr from-white/30 via-white/10 to-transparent opacity-45" />
1829
+ <div className="relative text-gray-900 prose prose-sm max-w-none whitespace-pre-wrap" dangerouslySetInnerHTML={{ __html: diffHtml }} />
1830
  </div>
1831
  </div>
1832
  )}
client/src/services/api.ts CHANGED
@@ -1,28 +1,76 @@
1
  import axios from 'axios';
2
 
3
- // Create axios instance with base configuration
4
- // Resolve base URL from environment, falling back to TransHub backend host
5
  const envUrlRaw = (process.env.REACT_APP_API_URL || '').trim();
6
  const envUrlNoTrailingSlash = envUrlRaw.replace(/\/$/, '');
7
- // If env provides .../api, strip the trailing /api so we can append /api ourselves consistently
8
  const envBaseWithoutApi = envUrlNoTrailingSlash.replace(/\/api$/, '');
9
- const resolvedBaseURL = envBaseWithoutApi || 'https://linguabot-transhub-backend.hf.space';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  const api = axios.create({
12
- baseURL: resolvedBaseURL,
13
  headers: {
14
  'Content-Type': 'application/json',
15
  },
16
  timeout: 10000, // 10 second timeout
17
  });
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  // Debug: Log the API URL being used
20
  console.log('🔧 API CONFIGURATION DEBUG');
21
  console.log('Environment variables:', {
22
  REACT_APP_API_URL: process.env.REACT_APP_API_URL,
23
  NODE_ENV: process.env.NODE_ENV
24
  });
25
- console.log('Resolved API Base URL:', resolvedBaseURL);
26
  console.log('Build timestamp:', new Date().toISOString());
27
 
28
  // Request interceptor to add auth token and user role
@@ -76,6 +124,24 @@ api.interceptors.response.use(
76
  (error) => {
77
  console.error('❌ API request failed:', error.config?.url, error.message);
78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  // Don't auto-redirect for admin operations - let the component handle it
80
  if (error.response?.status === 401 && !error.config?.url?.includes('/subtitles/update/')) {
81
  // Token expired or invalid - only redirect for non-admin operations
 
1
  import axios from 'axios';
2
 
3
+ // Create axios instance with dynamic base configuration and auto-discovery
4
+ // 1) Resolve env REACT_APP_API_URL (strip trailing /api if present)
5
  const envUrlRaw = (process.env.REACT_APP_API_URL || '').trim();
6
  const envUrlNoTrailingSlash = envUrlRaw.replace(/\/$/, '');
 
7
  const envBaseWithoutApi = envUrlNoTrailingSlash.replace(/\/api$/, '');
8
+ // 2) Prefer localhost if the app runs on localhost
9
+ const isLocalhost = typeof window !== 'undefined' && /^(localhost|127\.0\.0\.1)$/i.test(window.location.hostname);
10
+ const localCandidates = isLocalhost ? ['http://localhost:7860', 'http://127.0.0.1:7860', 'http://localhost:5000', 'http://127.0.0.1:5000'] : [];
11
+ // 3) HF backend as final fallback
12
+ const hfBackend = 'https://linguabot-transhub-backend.hf.space';
13
+ // 4) Build candidate list (deduped, truthy)
14
+ const candidateSet = new Set<string>([
15
+ envBaseWithoutApi,
16
+ ...localCandidates,
17
+ hfBackend
18
+ ].filter(Boolean) as string[]);
19
+ const candidates = Array.from(candidateSet);
20
+ // 5) Use persisted base if any
21
+ const storedBase = typeof window !== 'undefined' ? (localStorage.getItem('refinity_api_base') || '') : '';
22
+ let activeBase = storedBase && candidateSet.has(storedBase) ? storedBase : (candidates[0] || hfBackend);
23
 
24
  const api = axios.create({
25
+ baseURL: activeBase,
26
  headers: {
27
  'Content-Type': 'application/json',
28
  },
29
  timeout: 10000, // 10 second timeout
30
  });
31
 
32
+ // Helper to set and persist base URL used by both axios and fetch-derived code
33
+ function setActiveBase(nextBase: string) {
34
+ if (!nextBase) return;
35
+ activeBase = nextBase.replace(/\/$/, '');
36
+ api.defaults.baseURL = activeBase;
37
+ (api.defaults as any).baseURL = activeBase; // used by components to build fetch URLs
38
+ try { localStorage.setItem('refinity_api_base', activeBase); } catch {}
39
+ console.log('[Refinity] API base updated:', activeBase);
40
+ }
41
+
42
+ // Background probe to auto-select a reachable backend
43
+ async function probeBases() {
44
+ for (const base of candidates) {
45
+ try {
46
+ const controller = new AbortController();
47
+ const timer = setTimeout(() => controller.abort(), 2500);
48
+ const resp = await fetch(`${base.replace(/\/$/,'')}/api/health`, { signal: controller.signal });
49
+ clearTimeout(timer);
50
+ if (resp && resp.ok) {
51
+ if (activeBase !== base) setActiveBase(base);
52
+ return;
53
+ }
54
+ } catch {
55
+ // try next
56
+ }
57
+ }
58
+ // nothing reachable - keep current activeBase (likely HF)
59
+ }
60
+ // Kick off probe without blocking module init
61
+ if (typeof window !== 'undefined') {
62
+ // initialize from stored or first candidate, then probe
63
+ setActiveBase(activeBase);
64
+ probeBases();
65
+ }
66
+
67
  // Debug: Log the API URL being used
68
  console.log('🔧 API CONFIGURATION DEBUG');
69
  console.log('Environment variables:', {
70
  REACT_APP_API_URL: process.env.REACT_APP_API_URL,
71
  NODE_ENV: process.env.NODE_ENV
72
  });
73
+ console.log('Initial API Base URL:', activeBase);
74
  console.log('Build timestamp:', new Date().toISOString());
75
 
76
  // Request interceptor to add auth token and user role
 
124
  (error) => {
125
  console.error('❌ API request failed:', error.config?.url, error.message);
126
 
127
+ // If the current base fails with a network/cors error, rotate to next candidate and retry once
128
+ const isNetworkOrCORS = !error.response || error.message?.includes('Network') || error.message?.includes('Failed to fetch');
129
+ if (isNetworkOrCORS && typeof window !== 'undefined') {
130
+ const idx = candidates.indexOf(activeBase);
131
+ const nextIdx = (idx + 1) % candidates.length;
132
+ const nextBase = candidates[nextIdx];
133
+ if (nextBase && nextBase !== activeBase) {
134
+ setActiveBase(nextBase);
135
+ const cfg: any = error.config || {};
136
+ if (!cfg.__retriedWithNextBase) {
137
+ cfg.__retriedWithNextBase = true;
138
+ cfg.baseURL = activeBase;
139
+ console.warn('[Refinity] Retrying request with base:', activeBase);
140
+ return api.request(cfg);
141
+ }
142
+ }
143
+ }
144
+
145
  // Don't auto-redirect for admin operations - let the component handle it
146
  if (error.response?.status === 401 && !error.config?.url?.includes('/subtitles/update/')) {
147
  // Token expired or invalid - only redirect for non-admin operations