Upload folder using huggingface_hub
Browse files
client/src/components/Refinity.tsx
CHANGED
|
@@ -41,6 +41,7 @@ type Annotation = {
|
|
| 41 |
category: AnnotationCategory;
|
| 42 |
comment?: string;
|
| 43 |
correction?: string;
|
|
|
|
| 44 |
createdAt: number;
|
| 45 |
updatedAt: number;
|
| 46 |
};
|
|
@@ -1873,6 +1874,24 @@ const EditorPane: React.FC<{ source: string; initialTranslation: string; onBack:
|
|
| 1873 |
return () => window.removeEventListener('beforeunload', handler);
|
| 1874 |
}, [dirty]);
|
| 1875 |
const showToast = (msg: string) => { setToastMsg(msg); setTimeout(()=>setToastMsg(''), 1800); };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1876 |
const ANNO_STORE_KEY = 'refinity_annotations_v1';
|
| 1877 |
type Ann = Annotation;
|
| 1878 |
const loadAnnotations = React.useCallback((): Ann[] => {
|
|
@@ -1881,7 +1900,20 @@ const EditorPane: React.FC<{ source: string; initialTranslation: string; onBack:
|
|
| 1881 |
const saveAnnotations = React.useCallback((list: Ann[]) => { try { localStorage.setItem(ANNO_STORE_KEY, JSON.stringify(list)); } catch {} }, []);
|
| 1882 |
const [annotations, setAnnotations] = React.useState<Ann[]>(() => loadAnnotations());
|
| 1883 |
React.useEffect(() => { saveAnnotations(annotations); }, [annotations, saveAnnotations]);
|
| 1884 |
-
const versionAnnotations = React.useMemo(() =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1885 |
const addAnnotation = (a: Ann) => setAnnotations(prev => [...prev, a]);
|
| 1886 |
const updateAnnotation = (a: Ann) => setAnnotations(prev => prev.map(x => x.id === a.id ? a : x));
|
| 1887 |
const deleteAnnotationById = (id: string) => setAnnotations(prev => prev.filter(a => a.id !== id));
|
|
@@ -1929,6 +1961,18 @@ const EditorPane: React.FC<{ source: string; initialTranslation: string; onBack:
|
|
| 1929 |
// Only show annotations that either don't have createdBy (legacy) or were created by current user
|
| 1930 |
filteredRows = rows.filter((r: any) => !r.createdBy || r.createdBy.toLowerCase() === currentUsername);
|
| 1931 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1932 |
|
| 1933 |
setAnnotations(prev => {
|
| 1934 |
const others = prev.filter(a => a.versionId !== versionId);
|
|
@@ -1941,7 +1985,8 @@ const EditorPane: React.FC<{ source: string; initialTranslation: string; onBack:
|
|
| 1941 |
end: r.end,
|
| 1942 |
category: r.category,
|
| 1943 |
comment: r.comment,
|
| 1944 |
-
correction: existing?.correction,
|
|
|
|
| 1945 |
createdAt: r.createdAt,
|
| 1946 |
updatedAt: r.updatedAt,
|
| 1947 |
} as Ann;
|
|
@@ -2328,7 +2373,9 @@ const EditorPane: React.FC<{ source: string; initialTranslation: string; onBack:
|
|
| 2328 |
const base = initialTranslation || '';
|
| 2329 |
if (!versionAnnotations.length) return base;
|
| 2330 |
const anns = versionAnnotations
|
| 2331 |
-
|
|
|
|
|
|
|
| 2332 |
.slice()
|
| 2333 |
.sort((a, b) => a.start - b.start || a.end - b.end);
|
| 2334 |
if (!anns.length) return base;
|
|
@@ -2445,6 +2492,25 @@ const EditorPane: React.FC<{ source: string; initialTranslation: string; onBack:
|
|
| 2445 |
<div className="mb-2 text-gray-700 text-sm flex items-center justify-between">
|
| 2446 |
<span>Translation</span>
|
| 2447 |
<div className="flex items-center gap-2">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2448 |
{!isFullscreen && (
|
| 2449 |
<button onClick={onToggleFullscreen} className="inline-flex items-center px-2 py-1 text-xs rounded-xl bg-white/60 ring-1 ring-gray-200 text-gray-800 hover:bg-white">
|
| 2450 |
Full Screen
|
|
@@ -2511,7 +2577,8 @@ const EditorPane: React.FC<{ source: string; initialTranslation: string; onBack:
|
|
| 2511 |
const selected = text.slice(hit.start, hit.end);
|
| 2512 |
setModalSelectedText(selected);
|
| 2513 |
setModalCorrection(hit.correction ?? selected);
|
| 2514 |
-
|
|
|
|
| 2515 |
setModalSourceSnippet(
|
| 2516 |
computeSourceSnippetForOffset(source, text, hit.start)
|
| 2517 |
);
|
|
@@ -2522,6 +2589,12 @@ const EditorPane: React.FC<{ source: string; initialTranslation: string; onBack:
|
|
| 2522 |
setPopover(null);
|
| 2523 |
return;
|
| 2524 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2525 |
updateSelectionPopover();
|
| 2526 |
}, 10);
|
| 2527 |
}}
|
|
@@ -2539,6 +2612,7 @@ const EditorPane: React.FC<{ source: string; initialTranslation: string; onBack:
|
|
| 2539 |
<button
|
| 2540 |
type="button"
|
| 2541 |
onClick={()=>{
|
|
|
|
| 2542 |
setEditingAnn(null);
|
| 2543 |
setModalCategory('distortion');
|
| 2544 |
setModalComment('');
|
|
@@ -2559,6 +2633,7 @@ const EditorPane: React.FC<{ source: string; initialTranslation: string; onBack:
|
|
| 2559 |
setShowSourcePane(false);
|
| 2560 |
setModalOpen(true);
|
| 2561 |
}}
|
|
|
|
| 2562 |
className="relative overflow-hidden inline-flex items-center justify-center gap-2 px-3 py-1.5 text-xs font-medium rounded-2xl text-white ring-1 ring-inset ring-white/50 backdrop-blur-md backdrop-brightness-110 backdrop-saturate-150 bg-emerald-700 active:translate-y-0.5 transition-all duration-200"
|
| 2563 |
>
|
| 2564 |
<div className="pointer-events-none absolute inset-0 rounded-2xl opacity-60 [background:linear-gradient(to_bottom,rgba(255,255,255,0.35),rgba(255,255,255,0)_28%),linear-gradient(to_right,rgba(255,255,255,0.35),rgba(255,255,255,0)_28%)]" />
|
|
@@ -2568,7 +2643,7 @@ const EditorPane: React.FC<{ source: string; initialTranslation: string; onBack:
|
|
| 2568 |
</div>
|
| 2569 |
)}
|
| 2570 |
</div>
|
| 2571 |
-
<div className="mt-4 flex gap-3 relative">
|
| 2572 |
<button
|
| 2573 |
onClick={() => { setDirty(false); save(); showToast('Saved'); }}
|
| 2574 |
disabled={saving}
|
|
@@ -2830,12 +2905,24 @@ const EditorPane: React.FC<{ source: string; initialTranslation: string; onBack:
|
|
| 2830 |
</div>
|
| 2831 |
<div className="mt-6 flex items-center justify-between">
|
| 2832 |
{editingAnn && (
|
| 2833 |
-
<button
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2834 |
)}
|
| 2835 |
<div className="ml-auto flex gap-2">
|
| 2836 |
<button onClick={()=>{ setModalOpen(false); setPopover(null); setEditingAnn(null); setModalIsDeletion(false); }} className="px-3 py-1.5 text-sm rounded-lg border border-gray-300 text-gray-700 bg-white hover:bg-gray-50">Cancel</button>
|
| 2837 |
<button
|
| 2838 |
onClick={async ()=>{
|
|
|
|
| 2839 |
// Allow empty correction only if deletion is checked
|
| 2840 |
if (!modalIsDeletion && !modalCorrection.trim()) {
|
| 2841 |
alert('Please enter a correction for this highlighted text, or check "Delete this text" to mark it for deletion.');
|
|
@@ -2868,7 +2955,8 @@ const EditorPane: React.FC<{ source: string; initialTranslation: string; onBack:
|
|
| 2868 |
}
|
| 2869 |
setModalOpen(false); setPopover(null); setEditingAnn(null); setModalIsDeletion(false);
|
| 2870 |
}}
|
| 2871 |
-
|
|
|
|
| 2872 |
>
|
| 2873 |
Save
|
| 2874 |
</button>
|
|
|
|
| 41 |
category: AnnotationCategory;
|
| 42 |
comment?: string;
|
| 43 |
correction?: string;
|
| 44 |
+
createdBy?: string;
|
| 45 |
createdAt: number;
|
| 46 |
updatedAt: number;
|
| 47 |
};
|
|
|
|
| 1874 |
return () => window.removeEventListener('beforeunload', handler);
|
| 1875 |
}, [dirty]);
|
| 1876 |
const showToast = (msg: string) => { setToastMsg(msg); setTimeout(()=>setToastMsg(''), 1800); };
|
| 1877 |
+
// Tutorial admin: choose which user's highlights to display to avoid clashes.
|
| 1878 |
+
const ADMIN_ANNO_VIEW_CLEAN = '__clean__';
|
| 1879 |
+
const ADMIN_ANNO_VIEW_EDIT = '__edit__';
|
| 1880 |
+
const ADMIN_ANNO_VIEW_LEGACY = '__legacy__';
|
| 1881 |
+
const [adminAnnoView, setAdminAnnoView] = React.useState<string>(ADMIN_ANNO_VIEW_CLEAN);
|
| 1882 |
+
const [annoCreators, setAnnoCreators] = React.useState<string[]>([]);
|
| 1883 |
+
const isTutorialMode = (() => { try { return localStorage.getItem('refinityMode') === 'tutorial'; } catch { return false; } })();
|
| 1884 |
+
const currentUserLower = String(username || '').toLowerCase();
|
| 1885 |
+
const isAdminUser = (() => {
|
| 1886 |
+
try { return String(JSON.parse(localStorage.getItem('user') || '{}')?.role || '').toLowerCase() === 'admin'; } catch { return false; }
|
| 1887 |
+
})();
|
| 1888 |
+
const adminCanEditAnnotations = !(isTutorialMode && isAdminUser && adminAnnoView !== ADMIN_ANNO_VIEW_EDIT);
|
| 1889 |
+
|
| 1890 |
+
// Default admin view to clean on each version to avoid accidental carry-over.
|
| 1891 |
+
React.useEffect(() => {
|
| 1892 |
+
if (isTutorialMode && isAdminUser) setAdminAnnoView(ADMIN_ANNO_VIEW_CLEAN);
|
| 1893 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 1894 |
+
}, [versionId]);
|
| 1895 |
const ANNO_STORE_KEY = 'refinity_annotations_v1';
|
| 1896 |
type Ann = Annotation;
|
| 1897 |
const loadAnnotations = React.useCallback((): Ann[] => {
|
|
|
|
| 1900 |
const saveAnnotations = React.useCallback((list: Ann[]) => { try { localStorage.setItem(ANNO_STORE_KEY, JSON.stringify(list)); } catch {} }, []);
|
| 1901 |
const [annotations, setAnnotations] = React.useState<Ann[]>(() => loadAnnotations());
|
| 1902 |
React.useEffect(() => { saveAnnotations(annotations); }, [annotations, saveAnnotations]);
|
| 1903 |
+
const versionAnnotations = React.useMemo(() => {
|
| 1904 |
+
const base = annotations.filter(a => a.versionId === versionId);
|
| 1905 |
+
if (isTutorialMode) {
|
| 1906 |
+
if (isAdminUser) {
|
| 1907 |
+
if (adminAnnoView === ADMIN_ANNO_VIEW_CLEAN) return [];
|
| 1908 |
+
if (adminAnnoView === ADMIN_ANNO_VIEW_EDIT) return base.filter(a => String(a.createdBy || '').toLowerCase() === currentUserLower);
|
| 1909 |
+
if (adminAnnoView === ADMIN_ANNO_VIEW_LEGACY) return base.filter(a => !a.createdBy);
|
| 1910 |
+
return base.filter(a => String(a.createdBy || '').toLowerCase() === String(adminAnnoView || '').toLowerCase());
|
| 1911 |
+
}
|
| 1912 |
+
// Student (or non-admin): be defensive and only show own (or legacy) highlights.
|
| 1913 |
+
return base.filter(a => !a.createdBy || String(a.createdBy || '').toLowerCase() === currentUserLower);
|
| 1914 |
+
}
|
| 1915 |
+
return base;
|
| 1916 |
+
}, [annotations, versionId, isTutorialMode, isAdminUser, adminAnnoView, currentUserLower]);
|
| 1917 |
const addAnnotation = (a: Ann) => setAnnotations(prev => [...prev, a]);
|
| 1918 |
const updateAnnotation = (a: Ann) => setAnnotations(prev => prev.map(x => x.id === a.id ? a : x));
|
| 1919 |
const deleteAnnotationById = (id: string) => setAnnotations(prev => prev.filter(a => a.id !== id));
|
|
|
|
| 1961 |
// Only show annotations that either don't have createdBy (legacy) or were created by current user
|
| 1962 |
filteredRows = rows.filter((r: any) => !r.createdBy || r.createdBy.toLowerCase() === currentUsername);
|
| 1963 |
}
|
| 1964 |
+
|
| 1965 |
+
// For tutorial admin, capture the list of creators for the dropdown.
|
| 1966 |
+
if (isTutorial && isAdmin) {
|
| 1967 |
+
const creators = Array.from(new Set(
|
| 1968 |
+
rows
|
| 1969 |
+
.map((r: any) => (r?.createdBy ? String(r.createdBy).toLowerCase() : ''))
|
| 1970 |
+
.filter(Boolean)
|
| 1971 |
+
)).sort();
|
| 1972 |
+
setAnnoCreators(creators);
|
| 1973 |
+
} else {
|
| 1974 |
+
setAnnoCreators([]);
|
| 1975 |
+
}
|
| 1976 |
|
| 1977 |
setAnnotations(prev => {
|
| 1978 |
const others = prev.filter(a => a.versionId !== versionId);
|
|
|
|
| 1985 |
end: r.end,
|
| 1986 |
category: r.category,
|
| 1987 |
comment: r.comment,
|
| 1988 |
+
correction: (r.correction !== undefined ? r.correction : existing?.correction),
|
| 1989 |
+
createdBy: (r.createdBy !== undefined ? r.createdBy : existing?.createdBy),
|
| 1990 |
createdAt: r.createdAt,
|
| 1991 |
updatedAt: r.updatedAt,
|
| 1992 |
} as Ann;
|
|
|
|
| 2373 |
const base = initialTranslation || '';
|
| 2374 |
if (!versionAnnotations.length) return base;
|
| 2375 |
const anns = versionAnnotations
|
| 2376 |
+
// Include deletions: we represent them as empty-string corrections.
|
| 2377 |
+
// (Previously we filtered out empty corrections, which caused deletions to be ignored on save.)
|
| 2378 |
+
.filter(a => typeof a.correction === 'string')
|
| 2379 |
.slice()
|
| 2380 |
.sort((a, b) => a.start - b.start || a.end - b.end);
|
| 2381 |
if (!anns.length) return base;
|
|
|
|
| 2492 |
<div className="mb-2 text-gray-700 text-sm flex items-center justify-between">
|
| 2493 |
<span>Translation</span>
|
| 2494 |
<div className="flex items-center gap-2">
|
| 2495 |
+
{isTutorialMode && isAdminUser && !onSaveEdit && (
|
| 2496 |
+
<div className="flex items-center gap-2">
|
| 2497 |
+
<span className="text-xs text-gray-700">Highlights</span>
|
| 2498 |
+
<select
|
| 2499 |
+
value={adminAnnoView}
|
| 2500 |
+
onChange={(e)=>setAdminAnnoView(e.target.value)}
|
| 2501 |
+
className="h-8 px-2 text-sm rounded-lg border border-gray-300 bg-white"
|
| 2502 |
+
title="Select which user's highlights to display (admin-only)"
|
| 2503 |
+
>
|
| 2504 |
+
<option value={ADMIN_ANNO_VIEW_CLEAN}>Clean (none)</option>
|
| 2505 |
+
<option value={ADMIN_ANNO_VIEW_EDIT}>Edit (my highlights)</option>
|
| 2506 |
+
{annoCreators.length > 0 && <option disabled>──────────</option>}
|
| 2507 |
+
{annoCreators.map(u => (
|
| 2508 |
+
<option key={u} value={u}>{u}</option>
|
| 2509 |
+
))}
|
| 2510 |
+
<option value={ADMIN_ANNO_VIEW_LEGACY}>Legacy (unknown)</option>
|
| 2511 |
+
</select>
|
| 2512 |
+
</div>
|
| 2513 |
+
)}
|
| 2514 |
{!isFullscreen && (
|
| 2515 |
<button onClick={onToggleFullscreen} className="inline-flex items-center px-2 py-1 text-xs rounded-xl bg-white/60 ring-1 ring-gray-200 text-gray-800 hover:bg-white">
|
| 2516 |
Full Screen
|
|
|
|
| 2577 |
const selected = text.slice(hit.start, hit.end);
|
| 2578 |
setModalSelectedText(selected);
|
| 2579 |
setModalCorrection(hit.correction ?? selected);
|
| 2580 |
+
// If correction is empty-string, treat it as a deletion annotation.
|
| 2581 |
+
setModalIsDeletion(typeof hit.correction === 'string' && hit.correction.trim() === '');
|
| 2582 |
setModalSourceSnippet(
|
| 2583 |
computeSourceSnippetForOffset(source, text, hit.start)
|
| 2584 |
);
|
|
|
|
| 2589 |
setPopover(null);
|
| 2590 |
return;
|
| 2591 |
}
|
| 2592 |
+
if (!adminCanEditAnnotations) {
|
| 2593 |
+
// View-only mode: allow inspecting existing highlights (handled above),
|
| 2594 |
+
// but do not allow creating new highlights via selection.
|
| 2595 |
+
setPopover(null);
|
| 2596 |
+
return;
|
| 2597 |
+
}
|
| 2598 |
updateSelectionPopover();
|
| 2599 |
}, 10);
|
| 2600 |
}}
|
|
|
|
| 2612 |
<button
|
| 2613 |
type="button"
|
| 2614 |
onClick={()=>{
|
| 2615 |
+
if (!adminCanEditAnnotations) { showToast('Switch “Highlights” to Edit (my highlights) to add.'); return; }
|
| 2616 |
setEditingAnn(null);
|
| 2617 |
setModalCategory('distortion');
|
| 2618 |
setModalComment('');
|
|
|
|
| 2633 |
setShowSourcePane(false);
|
| 2634 |
setModalOpen(true);
|
| 2635 |
}}
|
| 2636 |
+
disabled={!adminCanEditAnnotations}
|
| 2637 |
className="relative overflow-hidden inline-flex items-center justify-center gap-2 px-3 py-1.5 text-xs font-medium rounded-2xl text-white ring-1 ring-inset ring-white/50 backdrop-blur-md backdrop-brightness-110 backdrop-saturate-150 bg-emerald-700 active:translate-y-0.5 transition-all duration-200"
|
| 2638 |
>
|
| 2639 |
<div className="pointer-events-none absolute inset-0 rounded-2xl opacity-60 [background:linear-gradient(to_bottom,rgba(255,255,255,0.35),rgba(255,255,255,0)_28%),linear-gradient(to_right,rgba(255,255,255,0.35),rgba(255,255,255,0)_28%)]" />
|
|
|
|
| 2643 |
</div>
|
| 2644 |
)}
|
| 2645 |
</div>
|
| 2646 |
+
<div className="mt-4 flex gap-3 relative items-center">
|
| 2647 |
<button
|
| 2648 |
onClick={() => { setDirty(false); save(); showToast('Saved'); }}
|
| 2649 |
disabled={saving}
|
|
|
|
| 2905 |
</div>
|
| 2906 |
<div className="mt-6 flex items-center justify-between">
|
| 2907 |
{editingAnn && (
|
| 2908 |
+
<button
|
| 2909 |
+
onClick={async()=>{
|
| 2910 |
+
if (!adminCanEditAnnotations) { showToast('Switch “Highlights” to Edit (my highlights) to modify.'); return; }
|
| 2911 |
+
await persistDelete(editingAnn.id);
|
| 2912 |
+
deleteAnnotationById(editingAnn.id);
|
| 2913 |
+
setModalOpen(false); setPopover(null); setEditingAnn(null); setModalIsDeletion(false);
|
| 2914 |
+
}}
|
| 2915 |
+
disabled={!adminCanEditAnnotations}
|
| 2916 |
+
className={`px-3 py-1.5 text-sm rounded-lg text-white ${adminCanEditAnnotations ? 'bg-red-600 hover:bg-red-700' : 'bg-gray-400 cursor-not-allowed'}`}
|
| 2917 |
+
>
|
| 2918 |
+
Delete
|
| 2919 |
+
</button>
|
| 2920 |
)}
|
| 2921 |
<div className="ml-auto flex gap-2">
|
| 2922 |
<button onClick={()=>{ setModalOpen(false); setPopover(null); setEditingAnn(null); setModalIsDeletion(false); }} className="px-3 py-1.5 text-sm rounded-lg border border-gray-300 text-gray-700 bg-white hover:bg-gray-50">Cancel</button>
|
| 2923 |
<button
|
| 2924 |
onClick={async ()=>{
|
| 2925 |
+
if (!adminCanEditAnnotations) { showToast('Switch “Highlights” to Edit (my highlights) to save changes.'); return; }
|
| 2926 |
// Allow empty correction only if deletion is checked
|
| 2927 |
if (!modalIsDeletion && !modalCorrection.trim()) {
|
| 2928 |
alert('Please enter a correction for this highlighted text, or check "Delete this text" to mark it for deletion.');
|
|
|
|
| 2955 |
}
|
| 2956 |
setModalOpen(false); setPopover(null); setEditingAnn(null); setModalIsDeletion(false);
|
| 2957 |
}}
|
| 2958 |
+
disabled={!adminCanEditAnnotations}
|
| 2959 |
+
className={`px-3 py-1.5 text-sm rounded-lg text-white ${adminCanEditAnnotations ? 'bg-indigo-600 hover:bg-indigo-700' : 'bg-gray-400 cursor-not-allowed'}`}
|
| 2960 |
>
|
| 2961 |
Save
|
| 2962 |
</button>
|