Upload folder using huggingface_hub
Browse files- client/src/components/Refinity.tsx +72 -24
- client/src/services/api.ts +72 -6
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(/[
|
|
|
|
| 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
|
| 1253 |
-
const
|
|
|
|
|
|
|
| 1254 |
const filename = `${toSafeName(task?.title||'Task')}_Compare-v${A}-v${B}_Inline_${yyyymmdd_hhmm()}_${toSafeName(latestReviser)}.docx`;
|
| 1255 |
-
const body = { prev:
|
| 1256 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 1272 |
-
const
|
|
|
|
|
|
|
| 1273 |
const filename = `${toSafeName(task?.title||'Task')}_Compare-v${A}-v${B}_Comments_${yyyymmdd_hhmm()}_${toSafeName(latestReviser)}.docx`;
|
| 1274 |
-
const body = { prev:
|
| 1275 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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">
|
| 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
|
| 1336 |
-
const
|
|
|
|
|
|
|
| 1337 |
const filename = `${toSafeName(task?.title||'Task')}_Compare-v${A}-v${B}_Inline_${yyyymmdd_hhmm()}_${toSafeName(latestReviser)}.docx`;
|
| 1338 |
-
const body = { prev: (
|
| 1339 |
-
const resp = await fetch(`${base}/api/refinity/track-changes
|
| 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
|
| 1354 |
-
const
|
|
|
|
|
|
|
| 1355 |
const filename = `${toSafeName(task?.title||'Task')}_Compare-v${A}-v${B}_Comments_${yyyymmdd_hhmm()}_${toSafeName(latestReviser)}.docx`;
|
| 1356 |
-
const body = { prev: (
|
| 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 (
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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 };
|
| 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 };
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
const api = axios.create({
|
| 12 |
-
baseURL:
|
| 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('
|
| 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
|