linguabot commited on
Commit
45ed7cb
·
verified ·
1 Parent(s): 92b4c1c

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. client/src/components/Refinity.tsx +95 -7
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(() => annotations.filter(a => a.versionId === versionId), [annotations, versionId]);
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- .filter(a => typeof a.correction === 'string' && a.correction.trim().length > 0)
 
 
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
- setModalIsDeletion(false);
 
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 onClick={async()=>{ await persistDelete(editingAnn.id); deleteAnnotationById(editingAnn.id); setModalOpen(false); setPopover(null); setEditingAnn(null); setModalIsDeletion(false); }} className="px-3 py-1.5 text-sm rounded-lg text-white bg-red-600 hover:bg-red-700">Delete</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
- className="px-3 py-1.5 text-sm rounded-lg text-white bg-indigo-600 hover:bg-indigo-700"
 
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>