Upload folder using huggingface_hub
Browse files- client/src/components/Refinity.tsx +104 -26
client/src/components/Refinity.tsx
CHANGED
|
@@ -68,10 +68,31 @@ const Refinity: React.FC = () => {
|
|
| 68 |
return 'task';
|
| 69 |
});
|
| 70 |
const [tasks, setTasks] = React.useState<Task[]>([]);
|
| 71 |
-
const [selectedTaskId, setSelectedTaskId] = React.useState<string>(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
const [versions, setVersions] = React.useState<Version[]>([]);
|
| 73 |
-
const [currentVersionId, setCurrentVersionId] = React.useState<string | null>(
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
const [isFullscreen, setIsFullscreen] = React.useState<boolean>(() => {
|
| 76 |
try {
|
| 77 |
const h = String(window.location.hash || '');
|
|
@@ -177,6 +198,10 @@ const Refinity: React.FC = () => {
|
|
| 177 |
const task = React.useMemo(() => tasks.find(t => t.id === selectedTaskId) || tasks[0], [tasks, selectedTaskId]);
|
| 178 |
const taskVersions = React.useMemo(() => versions.filter(v => v.taskId === (task?.id || '')), [versions, task?.id]);
|
| 179 |
// Load tasks
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
React.useEffect(() => {
|
| 181 |
(async () => {
|
| 182 |
try {
|
|
@@ -188,11 +213,27 @@ const Refinity: React.FC = () => {
|
|
| 188 |
initialRouteRef.current = { stage: desiredStage, taskId: params.taskId, versionId: params.versionId, previewId: params.previewId, fullscreen };
|
| 189 |
if (fullscreen) setIsFullscreen(true);
|
| 190 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
const base = ((api.defaults as any)?.baseURL as string || '').replace(/\/$/, '');
|
| 192 |
const resp = await fetch(`${base}/api/refinity/tasks`);
|
| 193 |
const data: any[] = await resp.json().catch(()=>[]);
|
| 194 |
const normalized: Task[] = Array.isArray(data) ? data.map(d => ({ id: d._id, title: d.title, sourceText: d.sourceText, createdBy: d.createdBy })) : [];
|
| 195 |
setTasks(normalized);
|
|
|
|
| 196 |
// Apply initial route task selection if available
|
| 197 |
const initTaskId = initialRouteRef.current?.taskId;
|
| 198 |
if (normalized.length) {
|
|
@@ -211,11 +252,23 @@ const Refinity: React.FC = () => {
|
|
| 211 |
(async () => {
|
| 212 |
if (!task?.id) { setVersions([]); return; }
|
| 213 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
const base = ((api.defaults as any)?.baseURL as string || '').replace(/\/$/, '');
|
| 215 |
const resp = await fetch(`${base}/api/refinity/tasks/${encodeURIComponent(task.id)}/versions`);
|
| 216 |
const data: any[] = await resp.json().catch(()=>[]);
|
| 217 |
const normalized: Version[] = Array.isArray(data) ? data.map(d => ({ id: d._id, taskId: d.taskId, originalAuthor: d.originalAuthor, revisedBy: d.revisedBy, versionNumber: d.versionNumber, content: d.content, parentVersionId: d.parentVersionId })) : [];
|
| 218 |
setVersions(normalized);
|
|
|
|
| 219 |
// Restore stage/version from initial route if present
|
| 220 |
const init = initialRouteRef.current;
|
| 221 |
if (init?.stage && normalized.length) {
|
|
@@ -227,18 +280,23 @@ const Refinity: React.FC = () => {
|
|
| 227 |
} else if (init.stage === 'preview' && init.previewId && normalized.some(v => v.id === init.previewId)) {
|
| 228 |
setPreviewVersionId(init.previewId);
|
| 229 |
setStage('preview');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
} else if (init.stage === 'flow') {
|
| 231 |
setStage('flow');
|
| 232 |
-
} else {
|
| 233 |
-
setCurrentVersionId(normalized[normalized.length-1].id);
|
| 234 |
-
setStage('flow');
|
| 235 |
}
|
| 236 |
} finally {
|
| 237 |
// small defer to avoid immediate hash writes during restore
|
| 238 |
setTimeout(() => { restoringRef.current = false; }, 50);
|
| 239 |
}
|
| 240 |
} else {
|
| 241 |
-
|
|
|
|
| 242 |
}
|
| 243 |
} catch { setVersions([]); }
|
| 244 |
})();
|
|
@@ -318,6 +376,8 @@ const Refinity: React.FC = () => {
|
|
| 318 |
return Math.min(Math.max(taskVersions.length - 1, 0), flowIndex);
|
| 319 |
}, [taskVersions, currentVersionId, flowIndex]);
|
| 320 |
const centerVersionId = React.useMemo(() => (taskVersions[flowIndex]?.id) || '', [taskVersions, flowIndex]);
|
|
|
|
|
|
|
| 321 |
|
| 322 |
// Keyboard navigation disabled per request (use arrow buttons only)
|
| 323 |
|
|
@@ -1290,29 +1350,43 @@ const Refinity: React.FC = () => {
|
|
| 1290 |
|
| 1291 |
{/* Stage: Preview full text */}
|
| 1292 |
{stage === 'preview' && (
|
| 1293 |
-
|
| 1294 |
-
|
| 1295 |
-
|
| 1296 |
-
|
| 1297 |
-
|
| 1298 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1299 |
)}
|
| 1300 |
|
| 1301 |
{/* Stage 3: Editor */}
|
| 1302 |
{stage === 'editor' && (
|
| 1303 |
-
|
| 1304 |
-
|
| 1305 |
-
|
| 1306 |
-
|
| 1307 |
-
|
| 1308 |
-
|
| 1309 |
-
|
| 1310 |
-
|
| 1311 |
-
|
| 1312 |
-
|
| 1313 |
-
|
| 1314 |
-
|
| 1315 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1316 |
)}
|
| 1317 |
</div>
|
| 1318 |
</div>
|
|
@@ -1322,6 +1396,10 @@ const Refinity: React.FC = () => {
|
|
| 1322 |
const EditorPane: React.FC<{ source: string; initialTranslation: string; onBack: ()=>void; onSave: (text: string)=>void; onSaveEdit?: (text: string)=>void; taskTitle: string; username: string; nextVersionNumber: number; isFullscreen: boolean; onToggleFullscreen: ()=>void; versionId: string }>=({ source, initialTranslation, onBack, onSave, onSaveEdit, taskTitle, username, nextVersionNumber, isFullscreen, onToggleFullscreen, versionId })=>{
|
| 1323 |
const [text, setText] = React.useState<string>(initialTranslation);
|
| 1324 |
const [saving, setSaving] = React.useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1325 |
const [diffHtml, setDiffHtml] = React.useState<string>('');
|
| 1326 |
const [showDiff, setShowDiff] = React.useState<boolean>(false);
|
| 1327 |
const [revDownloadOpen, setRevDownloadOpen] = React.useState<boolean>(false);
|
|
|
|
| 68 |
return 'task';
|
| 69 |
});
|
| 70 |
const [tasks, setTasks] = React.useState<Task[]>([]);
|
| 71 |
+
const [selectedTaskId, setSelectedTaskId] = React.useState<string>(() => {
|
| 72 |
+
try {
|
| 73 |
+
const h = String(window.location.hash || '');
|
| 74 |
+
const q = h.includes('?') ? h.slice(h.indexOf('?') + 1) : '';
|
| 75 |
+
const params = new URLSearchParams(q);
|
| 76 |
+
return String(params.get('taskId') || '');
|
| 77 |
+
} catch { return ''; }
|
| 78 |
+
});
|
| 79 |
const [versions, setVersions] = React.useState<Version[]>([]);
|
| 80 |
+
const [currentVersionId, setCurrentVersionId] = React.useState<string | null>(() => {
|
| 81 |
+
try {
|
| 82 |
+
const h = String(window.location.hash || '');
|
| 83 |
+
const q = h.includes('?') ? h.slice(h.indexOf('?') + 1) : '';
|
| 84 |
+
const params = new URLSearchParams(q);
|
| 85 |
+
return String(params.get('versionId') || '') || null;
|
| 86 |
+
} catch { return null; }
|
| 87 |
+
});
|
| 88 |
+
const [previewVersionId, setPreviewVersionId] = React.useState<string | null>(() => {
|
| 89 |
+
try {
|
| 90 |
+
const h = String(window.location.hash || '');
|
| 91 |
+
const q = h.includes('?') ? h.slice(h.indexOf('?') + 1) : '';
|
| 92 |
+
const params = new URLSearchParams(q);
|
| 93 |
+
return String(params.get('previewId') || '') || null;
|
| 94 |
+
} catch { return null; }
|
| 95 |
+
});
|
| 96 |
const [isFullscreen, setIsFullscreen] = React.useState<boolean>(() => {
|
| 97 |
try {
|
| 98 |
const h = String(window.location.hash || '');
|
|
|
|
| 198 |
const task = React.useMemo(() => tasks.find(t => t.id === selectedTaskId) || tasks[0], [tasks, selectedTaskId]);
|
| 199 |
const taskVersions = React.useMemo(() => versions.filter(v => v.taskId === (task?.id || '')), [versions, task?.id]);
|
| 200 |
// Load tasks
|
| 201 |
+
// Simple session cache keys
|
| 202 |
+
const TASKS_CACHE_KEY = 'refinity_tasks_cache_v1';
|
| 203 |
+
const versionsCacheKey = React.useCallback((taskId: string) => `refinity_versions_cache_${taskId}_v1`, []);
|
| 204 |
+
|
| 205 |
React.useEffect(() => {
|
| 206 |
(async () => {
|
| 207 |
try {
|
|
|
|
| 213 |
initialRouteRef.current = { stage: desiredStage, taskId: params.taskId, versionId: params.versionId, previewId: params.previewId, fullscreen };
|
| 214 |
if (fullscreen) setIsFullscreen(true);
|
| 215 |
}
|
| 216 |
+
// Hydrate from cache immediately
|
| 217 |
+
try {
|
| 218 |
+
const cached = JSON.parse(sessionStorage.getItem(TASKS_CACHE_KEY) || '[]');
|
| 219 |
+
if (Array.isArray(cached) && cached.length && tasks.length === 0) {
|
| 220 |
+
setTasks(cached);
|
| 221 |
+
if (!selectedTaskId) {
|
| 222 |
+
const initTaskId = initialRouteRef.current?.taskId;
|
| 223 |
+
if (initTaskId && cached.some((t:any)=>t.id===initTaskId)) {
|
| 224 |
+
setSelectedTaskId(initTaskId);
|
| 225 |
+
} else {
|
| 226 |
+
setSelectedTaskId(cached[0].id);
|
| 227 |
+
}
|
| 228 |
+
}
|
| 229 |
+
}
|
| 230 |
+
} catch {}
|
| 231 |
const base = ((api.defaults as any)?.baseURL as string || '').replace(/\/$/, '');
|
| 232 |
const resp = await fetch(`${base}/api/refinity/tasks`);
|
| 233 |
const data: any[] = await resp.json().catch(()=>[]);
|
| 234 |
const normalized: Task[] = Array.isArray(data) ? data.map(d => ({ id: d._id, title: d.title, sourceText: d.sourceText, createdBy: d.createdBy })) : [];
|
| 235 |
setTasks(normalized);
|
| 236 |
+
try { sessionStorage.setItem(TASKS_CACHE_KEY, JSON.stringify(normalized)); } catch {}
|
| 237 |
// Apply initial route task selection if available
|
| 238 |
const initTaskId = initialRouteRef.current?.taskId;
|
| 239 |
if (normalized.length) {
|
|
|
|
| 252 |
(async () => {
|
| 253 |
if (!task?.id) { setVersions([]); return; }
|
| 254 |
try {
|
| 255 |
+
// Hydrate versions from cache for instant UI
|
| 256 |
+
try {
|
| 257 |
+
const cached = JSON.parse(sessionStorage.getItem(versionsCacheKey(task.id)) || '[]');
|
| 258 |
+
if (Array.isArray(cached) && cached.length) {
|
| 259 |
+
setVersions(cached);
|
| 260 |
+
// If versionId missing but stage requests editor, default to latest cached
|
| 261 |
+
if (stage === 'editor' && !currentVersionId) {
|
| 262 |
+
setCurrentVersionId(cached[cached.length - 1]?.id || null);
|
| 263 |
+
}
|
| 264 |
+
}
|
| 265 |
+
} catch {}
|
| 266 |
const base = ((api.defaults as any)?.baseURL as string || '').replace(/\/$/, '');
|
| 267 |
const resp = await fetch(`${base}/api/refinity/tasks/${encodeURIComponent(task.id)}/versions`);
|
| 268 |
const data: any[] = await resp.json().catch(()=>[]);
|
| 269 |
const normalized: Version[] = Array.isArray(data) ? data.map(d => ({ id: d._id, taskId: d.taskId, originalAuthor: d.originalAuthor, revisedBy: d.revisedBy, versionNumber: d.versionNumber, content: d.content, parentVersionId: d.parentVersionId })) : [];
|
| 270 |
setVersions(normalized);
|
| 271 |
+
try { sessionStorage.setItem(versionsCacheKey(task.id), JSON.stringify(normalized)); } catch {}
|
| 272 |
// Restore stage/version from initial route if present
|
| 273 |
const init = initialRouteRef.current;
|
| 274 |
if (init?.stage && normalized.length) {
|
|
|
|
| 280 |
} else if (init.stage === 'preview' && init.previewId && normalized.some(v => v.id === init.previewId)) {
|
| 281 |
setPreviewVersionId(init.previewId);
|
| 282 |
setStage('preview');
|
| 283 |
+
} else if (init.stage === 'editor') {
|
| 284 |
+
// Stay in editor; choose latest if specific version missing
|
| 285 |
+
setCurrentVersionId(normalized[normalized.length-1]?.id || null);
|
| 286 |
+
setStage('editor');
|
| 287 |
+
} else if (init.stage === 'preview') {
|
| 288 |
+
setPreviewVersionId(normalized[normalized.length-1]?.id || null);
|
| 289 |
+
setStage('preview');
|
| 290 |
} else if (init.stage === 'flow') {
|
| 291 |
setStage('flow');
|
|
|
|
|
|
|
|
|
|
| 292 |
}
|
| 293 |
} finally {
|
| 294 |
// small defer to avoid immediate hash writes during restore
|
| 295 |
setTimeout(() => { restoringRef.current = false; }, 50);
|
| 296 |
}
|
| 297 |
} else {
|
| 298 |
+
// No init; do not force stage change if already 'editor' or 'preview'
|
| 299 |
+
if (!currentVersionId) setCurrentVersionId(normalized.length ? normalized[normalized.length-1].id : null);
|
| 300 |
}
|
| 301 |
} catch { setVersions([]); }
|
| 302 |
})();
|
|
|
|
| 376 |
return Math.min(Math.max(taskVersions.length - 1, 0), flowIndex);
|
| 377 |
}, [taskVersions, currentVersionId, flowIndex]);
|
| 378 |
const centerVersionId = React.useMemo(() => (taskVersions[flowIndex]?.id) || '', [taskVersions, flowIndex]);
|
| 379 |
+
const currentVersion = React.useMemo(() => taskVersions.find(v => v.id === currentVersionId) || null, [taskVersions, currentVersionId]);
|
| 380 |
+
const previewVersion = React.useMemo(() => taskVersions.find(v => v.id === previewVersionId) || null, [taskVersions, previewVersionId]);
|
| 381 |
|
| 382 |
// Keyboard navigation disabled per request (use arrow buttons only)
|
| 383 |
|
|
|
|
| 1350 |
|
| 1351 |
{/* Stage: Preview full text */}
|
| 1352 |
{stage === 'preview' && (
|
| 1353 |
+
<>
|
| 1354 |
+
{!previewVersion && (
|
| 1355 |
+
<div className="rounded-xl ring-1 ring-gray-200 shadow-sm overflow-hidden bg-white/60 min-h-[280px] animate-pulse" />
|
| 1356 |
+
)}
|
| 1357 |
+
{previewVersion && (
|
| 1358 |
+
<PreviewPane
|
| 1359 |
+
version={previewVersion || (flowIndex >= 0 ? (taskVersions[flowIndex] || null) : null)}
|
| 1360 |
+
onBack={()=>setStage('flow')}
|
| 1361 |
+
onEdit={()=>{ if (previewVersionId) { setCurrentVersionId(previewVersionId); setStage('editor'); } }}
|
| 1362 |
+
compareInitially={showPreviewDiff}
|
| 1363 |
+
/>
|
| 1364 |
+
)}
|
| 1365 |
+
</>
|
| 1366 |
)}
|
| 1367 |
|
| 1368 |
{/* Stage 3: Editor */}
|
| 1369 |
{stage === 'editor' && (
|
| 1370 |
+
<>
|
| 1371 |
+
{(!currentVersion || !task?.sourceText) && (
|
| 1372 |
+
<div className="rounded-xl ring-1 ring-gray-200 shadow-sm overflow-hidden bg-white/60 min-h-[520px] animate-pulse" />
|
| 1373 |
+
)}
|
| 1374 |
+
{currentVersion && task?.sourceText && (
|
| 1375 |
+
<EditorPane
|
| 1376 |
+
source={task?.sourceText || ''}
|
| 1377 |
+
initialTranslation={currentVersion?.content || ''}
|
| 1378 |
+
onBack={()=>setStage('flow')}
|
| 1379 |
+
onSave={handleSaveRevision}
|
| 1380 |
+
onSaveEdit={editingVersionId ? ((text)=>saveEditedVersion(editingVersionId, text)) : undefined}
|
| 1381 |
+
taskTitle={task?.title || 'Task'}
|
| 1382 |
+
username={username}
|
| 1383 |
+
nextVersionNumber={((versions.filter(v=>v.taskId===(task?.id||'')).slice(-1)[0]?.versionNumber) || 0) + 1}
|
| 1384 |
+
isFullscreen={isFullscreen}
|
| 1385 |
+
onToggleFullscreen={()=>setIsFullscreen(v=>!v)}
|
| 1386 |
+
versionId={currentVersionId || ''}
|
| 1387 |
+
/>
|
| 1388 |
+
)}
|
| 1389 |
+
</>
|
| 1390 |
)}
|
| 1391 |
</div>
|
| 1392 |
</div>
|
|
|
|
| 1396 |
const EditorPane: React.FC<{ source: string; initialTranslation: string; onBack: ()=>void; onSave: (text: string)=>void; onSaveEdit?: (text: string)=>void; taskTitle: string; username: string; nextVersionNumber: number; isFullscreen: boolean; onToggleFullscreen: ()=>void; versionId: string }>=({ source, initialTranslation, onBack, onSave, onSaveEdit, taskTitle, username, nextVersionNumber, isFullscreen, onToggleFullscreen, versionId })=>{
|
| 1397 |
const [text, setText] = React.useState<string>(initialTranslation);
|
| 1398 |
const [saving, setSaving] = React.useState(false);
|
| 1399 |
+
// Sync text with incoming props when version/context changes (e.g., refresh -> data loads)
|
| 1400 |
+
React.useEffect(() => {
|
| 1401 |
+
setText(initialTranslation || '');
|
| 1402 |
+
}, [versionId, initialTranslation]);
|
| 1403 |
const [diffHtml, setDiffHtml] = React.useState<string>('');
|
| 1404 |
const [showDiff, setShowDiff] = React.useState<boolean>(false);
|
| 1405 |
const [revDownloadOpen, setRevDownloadOpen] = React.useState<boolean>(false);
|