Upload folder using huggingface_hub
Browse files
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 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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">
|