linguabot commited on
Commit
c9a31ac
·
verified ·
1 Parent(s): e0aeefd

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. client/src/components/Refinity.tsx +103 -6
client/src/components/Refinity.tsx CHANGED
@@ -157,6 +157,16 @@ const Refinity: React.FC = () => {
157
  const [compareMenuPos, setCompareMenuPos] = React.useState<{ left: number; top: number } | null>(null);
158
  const compareBtnRef = React.useRef<HTMLButtonElement | null>(null);
159
  const [revDownloadOpen, setRevDownloadOpen] = React.useState<boolean>(false);
 
 
 
 
 
 
 
 
 
 
160
  // --- Route persistence (hash-based) ---
161
  const restoringRef = React.useRef<boolean>(false);
162
  const appliedInitialRouteRef = React.useRef<boolean>(false);
@@ -751,7 +761,7 @@ const Refinity: React.FC = () => {
751
  };
752
  setVersions(prev => [...prev, v]);
753
  setCurrentVersionId(v.id);
754
- setCurrentVersionId(null);
755
  setStage('flow');
756
  } catch {
757
  // no-op; keep user in editor if needed
@@ -1339,6 +1349,9 @@ const Refinity: React.FC = () => {
1339
  .mySwiper .swiper-button-prev, .mySwiper .swiper-button-next { top: 50% !important; transform: translateY(-50%); }
1340
  .mySwiper .swiper-button-prev:after, .mySwiper .swiper-button-next:after { font-size: 18px; color: #94a3b8; }
1341
  .mySwiper .swiper-pagination { bottom: 6px; }
 
 
 
1342
  /* Ensure buttons inside slides are clickable */
1343
  .mySwiper .swiper-slide button, .mySwiper .swiper-slide .action-row > * {
1344
  pointer-events: auto !important;
@@ -1490,6 +1503,54 @@ const EditorPane: React.FC<{ source: string; initialTranslation: string; onBack:
1490
  const wrapperRef = React.useRef<HTMLDivElement | null>(null);
1491
  // Annotation layer state (revision mode only)
1492
  const [showAnnotations, setShowAnnotations] = React.useState<boolean>(true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1493
  const ANNO_STORE_KEY = 'refinity_annotations_v1';
1494
  type Ann = Annotation;
1495
  const loadAnnotations = React.useCallback((): Ann[] => {
@@ -1713,6 +1774,8 @@ const EditorPane: React.FC<{ source: string; initialTranslation: string; onBack:
1713
  <div className="flex items-start gap-6">
1714
  <div className="w-1/2">
1715
  <div className="mb-2 text-gray-700 text-sm">Source</div>
 
 
1716
  <div className="relative rounded-lg overflow-hidden">
1717
  <div className="absolute inset-0 rounded-lg" style={{ background: 'radial-gradient(120%_120%_at_0%_0%, #dbeafe 0%, #ffffff 50%, #c7d2fe 100%)' }} />
1718
  <div ref={sourceRef} className="relative rounded-lg bg-white/60 backdrop-blur-md ring-1 ring-inset ring-indigo-300 shadow p-4 min-h-[420px] whitespace-pre-wrap text-gray-900">
@@ -1735,7 +1798,15 @@ const EditorPane: React.FC<{ source: string; initialTranslation: string; onBack:
1735
  )}
1736
  </div>
1737
  </div>
1738
- <div ref={wrapperRef} className="relative overflow-hidden">
 
 
 
 
 
 
 
 
1739
  {/* Overlay highlights rendered above, but non-interactive; textarea handles selection */}
1740
  <div
1741
  ref={overlayRef}
@@ -1746,7 +1817,13 @@ const EditorPane: React.FC<{ source: string; initialTranslation: string; onBack:
1746
  <textarea
1747
  ref={taRef}
1748
  value={text}
1749
- onChange={(e)=>setText(e.target.value)}
 
 
 
 
 
 
1750
  onMouseUp={()=>{
1751
  updateSelectionPopover();
1752
  // If caret inside an existing annotation, open edit modal
@@ -1773,7 +1850,8 @@ const EditorPane: React.FC<{ source: string; initialTranslation: string; onBack:
1773
  overflowY: 'auto',
1774
  background: 'transparent',
1775
  color: '#111827' as any,
1776
- caretColor: '#111827'
 
1777
  }}
1778
  />
1779
  {/* + Comment popover */}
@@ -1797,7 +1875,23 @@ const EditorPane: React.FC<{ source: string; initialTranslation: string; onBack:
1797
  )}
1798
  </div>
1799
  <div className="mt-4 flex gap-3 relative">
1800
- <button onClick={save} disabled={saving} className="relative overflow-hidden inline-flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium rounded-2xl text-white ring-1 ring-inset ring-white/50 backdrop-blur-md backdrop-brightness-110 backdrop-saturate-150 bg-indigo-600/70 disabled:bg-gray-400 active:translate-y-0.5 transition-all duration-200">{saving? 'Saving…':'Save'}</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1801
  <div className="relative inline-block align-top">
1802
  <button
1803
  ref={revBtnRef}
@@ -1823,7 +1917,7 @@ const EditorPane: React.FC<{ source: string; initialTranslation: string; onBack:
1823
  )}
1824
  </div>
1825
  {/* Comments button removed per request */}
1826
- <button onClick={onBack} className="ml-auto relative overflow-hidden inline-flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium rounded-2xl text-black ring-1 ring-inset ring-white/50 backdrop-blur-md bg-white/30 active:translate-y-0.5 transition-all duration-200">Back</button>
1827
  </div>
1828
  {/* Inline comments drawer removed */}
1829
  {showDiff && (
@@ -1838,6 +1932,9 @@ const EditorPane: React.FC<{ source: string; initialTranslation: string; onBack:
1838
  )}
1839
  </div>
1840
  </div>
 
 
 
1841
  {/* Annotation modal */}
1842
  {modalOpen && (
1843
  <div className="fixed inset-0 z-[5000] flex items-center justify-center bg-black/40">
 
157
  const [compareMenuPos, setCompareMenuPos] = React.useState<{ left: number; top: number } | null>(null);
158
  const compareBtnRef = React.useRef<HTMLButtonElement | null>(null);
159
  const [revDownloadOpen, setRevDownloadOpen] = React.useState<boolean>(false);
160
+ // Chrome-only UA flag for layout tweaks that should not affect Safari/Firefox/Edge
161
+ React.useEffect(() => {
162
+ try {
163
+ const ua = navigator.userAgent || '';
164
+ const isChrome = /Chrome\//.test(ua) && !/Edg\//.test(ua) && !/OPR\//.test(ua);
165
+ const root = document.documentElement;
166
+ if (isChrome) root.classList.add('is-chrome');
167
+ else root.classList.remove('is-chrome');
168
+ } catch {}
169
+ }, []);
170
  // --- Route persistence (hash-based) ---
171
  const restoringRef = React.useRef<boolean>(false);
172
  const appliedInitialRouteRef = React.useRef<boolean>(false);
 
761
  };
762
  setVersions(prev => [...prev, v]);
763
  setCurrentVersionId(v.id);
764
+ // Navigate to flow to view the new version without clearing selection
765
  setStage('flow');
766
  } catch {
767
  // no-op; keep user in editor if needed
 
1349
  .mySwiper .swiper-button-prev, .mySwiper .swiper-button-next { top: 50% !important; transform: translateY(-50%); }
1350
  .mySwiper .swiper-button-prev:after, .mySwiper .swiper-button-next:after { font-size: 18px; color: #94a3b8; }
1351
  .mySwiper .swiper-pagination { bottom: 6px; }
1352
+ /* Chrome-only: avoid half-line clipping at the bottom of slides */
1353
+ .is-chrome .mySwiper .swiper-slide { padding-bottom: 6px; }
1354
+ .is-chrome .mySwiper .swiper-slide .whitespace-pre-wrap { padding-bottom: 6px; }
1355
  /* Ensure buttons inside slides are clickable */
1356
  .mySwiper .swiper-slide button, .mySwiper .swiper-slide .action-row > * {
1357
  pointer-events: auto !important;
 
1503
  const wrapperRef = React.useRef<HTMLDivElement | null>(null);
1504
  // Annotation layer state (revision mode only)
1505
  const [showAnnotations, setShowAnnotations] = React.useState<boolean>(true);
1506
+ // Comment-first workflow state (per-version; persisted in sessionStorage)
1507
+ const [commentsSaved, setCommentsSaved] = React.useState<boolean>(false);
1508
+ React.useEffect(() => {
1509
+ try {
1510
+ const v = sessionStorage.getItem(`refinity_comments_saved_${versionId}`) === '1';
1511
+ setCommentsSaved(!!v);
1512
+ } catch { setCommentsSaved(false); }
1513
+ }, [versionId]);
1514
+ const [toastMsg, setToastMsg] = React.useState<string>('');
1515
+ const [dirty, setDirty] = React.useState<boolean>(false);
1516
+ React.useEffect(() => { setDirty(false); }, [versionId, initialTranslation]);
1517
+ React.useEffect(() => {
1518
+ const handler = (e: BeforeUnloadEvent) => {
1519
+ if (dirty) { e.preventDefault(); e.returnValue = ''; }
1520
+ };
1521
+ window.addEventListener('beforeunload', handler);
1522
+ return () => window.removeEventListener('beforeunload', handler);
1523
+ }, [dirty]);
1524
+ const showToast = (msg: string) => { setToastMsg(msg); setTimeout(()=>setToastMsg(''), 1800); };
1525
+ const statusRef = React.useRef<HTMLDivElement | null>(null);
1526
+ const [statusH, setStatusH] = React.useState<number>(0);
1527
+ React.useLayoutEffect(() => {
1528
+ const el = statusRef.current;
1529
+ if (!el) { setStatusH(0); return; }
1530
+ const compute = () => {
1531
+ const h = el.offsetHeight || 0;
1532
+ let mt = 0, mb = 0;
1533
+ try {
1534
+ const cs = window.getComputedStyle(el);
1535
+ mt = parseFloat(cs.marginTop || '0') || 0;
1536
+ mb = parseFloat(cs.marginBottom || '0') || 0;
1537
+ } catch {}
1538
+ setStatusH(h + mt + mb);
1539
+ };
1540
+ compute();
1541
+ // Track dynamic changes without layout flicker
1542
+ let ro: ResizeObserver | null = null;
1543
+ try {
1544
+ ro = new ResizeObserver(() => compute());
1545
+ ro.observe(el);
1546
+ } catch {}
1547
+ const onResize = () => compute();
1548
+ window.addEventListener('resize', onResize);
1549
+ return () => {
1550
+ window.removeEventListener('resize', onResize);
1551
+ if (ro) try { ro.disconnect(); } catch {}
1552
+ };
1553
+ }, [commentsSaved]);
1554
  const ANNO_STORE_KEY = 'refinity_annotations_v1';
1555
  type Ann = Annotation;
1556
  const loadAnnotations = React.useCallback((): Ann[] => {
 
1774
  <div className="flex items-start gap-6">
1775
  <div className="w-1/2">
1776
  <div className="mb-2 text-gray-700 text-sm">Source</div>
1777
+ {/* Spacer to align with translation status bar (exact measured height) */}
1778
+ <div style={{ height: Math.max(0, statusH) }} />
1779
  <div className="relative rounded-lg overflow-hidden">
1780
  <div className="absolute inset-0 rounded-lg" style={{ background: 'radial-gradient(120%_120%_at_0%_0%, #dbeafe 0%, #ffffff 50%, #c7d2fe 100%)' }} />
1781
  <div ref={sourceRef} className="relative rounded-lg bg-white/60 backdrop-blur-md ring-1 ring-inset ring-indigo-300 shadow p-4 min-h-[420px] whitespace-pre-wrap text-gray-900">
 
1798
  )}
1799
  </div>
1800
  </div>
1801
+ {/* Status bar */}
1802
+ <div ref={statusRef} className="mb-2 text-xs">
1803
+ {!commentsSaved ? (
1804
+ <div className="rounded-md bg-amber-50 text-amber-900 ring-1 ring-amber-200 px-3 py-1">Step 1: Add comments. Save to continue to editing.</div>
1805
+ ) : (
1806
+ <div className="rounded-md bg-emerald-50 text-emerald-900 ring-1 ring-emerald-200 px-3 py-1">Comments saved. Editing unlocked.</div>
1807
+ )}
1808
+ </div>
1809
+ <div ref={wrapperRef} className="relative overflow-hidden">
1810
  {/* Overlay highlights rendered above, but non-interactive; textarea handles selection */}
1811
  <div
1812
  ref={overlayRef}
 
1817
  <textarea
1818
  ref={taRef}
1819
  value={text}
1820
+ readOnly={!commentsSaved}
1821
+ onChange={async (e)=>{
1822
+ const val = e.target.value;
1823
+ if (!commentsSaved) return; // locked
1824
+ setDirty(true);
1825
+ setText(val);
1826
+ }}
1827
  onMouseUp={()=>{
1828
  updateSelectionPopover();
1829
  // If caret inside an existing annotation, open edit modal
 
1850
  overflowY: 'auto',
1851
  background: 'transparent',
1852
  color: '#111827' as any,
1853
+ caretColor: '#111827',
1854
+ opacity: !commentsSaved ? 0.85 : 1
1855
  }}
1856
  />
1857
  {/* + Comment popover */}
 
1875
  )}
1876
  </div>
1877
  <div className="mt-4 flex gap-3 relative">
1878
+ <button
1879
+ onClick={()=>{
1880
+ setCommentsSaved(true);
1881
+ try { sessionStorage.setItem(`refinity_comments_saved_${versionId}`,'1'); } catch {}
1882
+ showToast('Comments saved — you can now edit the text.');
1883
+ }}
1884
+ className="relative overflow-hidden inline-flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium rounded-2xl text-white ring-1 ring-inset ring-white/50 backdrop-blur-md backdrop-brightness-110 backdrop-saturate-150 bg-emerald-600/80 hover:bg-emerald-700 active:translate-y-0.5 transition-all duration-200"
1885
+ >
1886
+ <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%)]" />
1887
+ <div className="pointer-events-none absolute inset-0 rounded-2xl bg-gradient-to-tr from-white/30 via-white/10 to-transparent opacity-50" />
1888
+ Save Comments
1889
+ </button>
1890
+ <button title={!commentsSaved ? 'Save comments to enable editing.' : undefined} onClick={() => { setDirty(false); save(); }} disabled={saving || !commentsSaved} className="relative overflow-hidden inline-flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium rounded-2xl text-white ring-1 ring-inset ring-white/50 backdrop-blur-md backdrop-brightness-110 backdrop-saturate-150 bg-indigo-600/70 hover:bg-indigo-700 disabled:bg-gray-400 active:translate-y-0.5 transition-all duration-200">
1891
+ <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%)]" />
1892
+ <div className="pointer-events-none absolute inset-0 rounded-2xl bg-gradient-to-tr from-white/30 via-white/10 to-transparent opacity-50" />
1893
+ {saving? 'Saving…':'Save Version'}
1894
+ </button>
1895
  <div className="relative inline-block align-top">
1896
  <button
1897
  ref={revBtnRef}
 
1917
  )}
1918
  </div>
1919
  {/* Comments button removed per request */}
1920
+ <button onClick={()=>{ if (!dirty || window.confirm('Discard unsaved changes?')) { setDirty(false); onBack(); } }} className="ml-auto relative overflow-hidden inline-flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium rounded-2xl text-black ring-1 ring-inset ring-white/50 backdrop-blur-md bg-white/30 active:translate-y-0.5 transition-all duration-200">Back</button>
1921
  </div>
1922
  {/* Inline comments drawer removed */}
1923
  {showDiff && (
 
1932
  )}
1933
  </div>
1934
  </div>
1935
+ {toastMsg && (
1936
+ <div className="fixed bottom-5 right-5 z-[6000] px-3 py-2 rounded-lg bg-black/80 text-white text-sm shadow-lg">{toastMsg}</div>
1937
+ )}
1938
  {/* Annotation modal */}
1939
  {modalOpen && (
1940
  <div className="fixed inset-0 z-[5000] flex items-center justify-center bg-black/40">