|
|
@@ -66,6 +66,9 @@ export default function ReviewPage() {
|
|
|
const [compareMismatch, setCompareMismatch] = useState<string | null>(null);
|
|
|
const [compareComments, setCompareComments] = useState<Comment[]>([]);
|
|
|
const [playing, setPlaying] = useState(false);
|
|
|
+ // Toggle annotation + speech bubble visibility per video in compare mode
|
|
|
+ const [showMainAnnotations, setShowMainAnnotations] = useState(true);
|
|
|
+ const [showCompareAnnotations, setShowCompareAnnotations] = useState(true);
|
|
|
|
|
|
const handleCompareSelect = useCallback((compareAssetArg: Asset) => {
|
|
|
setShowComparePicker(false);
|
|
|
@@ -590,113 +593,69 @@ export default function ReviewPage() {
|
|
|
|
|
|
{/* ── Side-by-side compare layout ───────────────────────── */}
|
|
|
{compareMode ? (
|
|
|
- <div className="flex gap-2 w-full">
|
|
|
+ <div className="flex gap-2 w-full flex-1 min-h-0">
|
|
|
{/* Main video + its comments */}
|
|
|
- <div className="flex-1 min-w-0 flex flex-col gap-0">
|
|
|
+ <div className="flex-1 min-w-0 flex flex-col gap-0 min-h-0">
|
|
|
+ {/* Annotation toggle */}
|
|
|
+ <div className="flex items-center gap-2 mb-1 px-1">
|
|
|
+ <button
|
|
|
+ onClick={() => setShowMainAnnotations(v => !v)}
|
|
|
+ className="flex items-center gap-1.5 text-[11px] px-2 py-1 rounded-md transition-colors"
|
|
|
+ style={showMainAnnotations
|
|
|
+ ? { background: 'rgba(99,102,241,0.15)', color: '#818CF8' }
|
|
|
+ : { background: 'rgba(255,255,255,0.05)', color: 'var(--text-subtle)' }}
|
|
|
+ title={showMainAnnotations ? 'Hide annotations' : 'Show annotations'}
|
|
|
+ >
|
|
|
+ <svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
|
+ <path strokeLinecap="round" strokeLinejoin="round" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
|
|
+ </svg>
|
|
|
+ Annot.
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
<div className="text-xs mb-1 px-1 truncate" style={{ color: 'rgba(255,255,255,0.5)' }}>
|
|
|
{asset.title}
|
|
|
</div>
|
|
|
- <VideoPlayer
|
|
|
- src={videoUrl}
|
|
|
- mimeType={asset.mimeType}
|
|
|
- fps={fps}
|
|
|
- comments={allComments}
|
|
|
- visibleAnnotations={visibleAnnotations}
|
|
|
- drawMode={drawMode}
|
|
|
- drawTool={drawTool}
|
|
|
- drawColor={drawColor}
|
|
|
- onDrawModeChange={setDrawMode}
|
|
|
- onDrawToolChange={setDrawTool}
|
|
|
- onDrawColorChange={setDrawColor}
|
|
|
- pendingStrokes={pendingStrokes}
|
|
|
- onStrokeComplete={handleStrokeComplete}
|
|
|
- onTimeUpdate={handleTimeUpdate}
|
|
|
- onCommentClick={handleCommentSeek}
|
|
|
- onPlayingChange={setPlaying}
|
|
|
- onTimelineSeek={handleTimeUpdate}
|
|
|
- externalCurrentTime={currentTime}
|
|
|
- externalPlaying={playing}
|
|
|
- />
|
|
|
- {/* Comments below main video */}
|
|
|
- <div className="mt-2 rounded-xl overflow-hidden" style={{ background: 'rgba(10,11,20,0.80)', border: '1px solid rgba(255,255,255,0.06)' }}>
|
|
|
- <div className="px-3 py-2 flex items-center gap-2" style={{ borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
|
|
|
- <span className="text-xs font-medium" style={{ color: 'var(--text)' }}>
|
|
|
- Comments
|
|
|
- </span>
|
|
|
- <span className="text-xs px-1.5 py-0.5 rounded-full" style={{ background: 'rgba(255,255,255,0.06)', color: 'var(--text-muted)' }}>
|
|
|
- {visibleComments.length}
|
|
|
- </span>
|
|
|
- <span className="font-mono text-[11px] ml-auto" style={{ color: '#818CF8' }}>
|
|
|
- {formatTimecode(currentTime, fps, asset?.duration ?? 0)}
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- <div className="max-h-32 overflow-y-auto">
|
|
|
- {visibleComments.length === 0 ? (
|
|
|
- <p className="text-xs text-center py-4" style={{ color: 'var(--text-muted)' }}>No comments</p>
|
|
|
- ) : (
|
|
|
- visibleComments.slice(0, 5).map(comment => (
|
|
|
- <div key={comment.id} className="px-3 py-2 flex items-start gap-2" style={{ borderBottom: '1px solid rgba(255,255,255,0.04)' }}>
|
|
|
- <Avatar name={comment.user?.name ?? 'U'} size="xs" />
|
|
|
- <div className="flex-1 min-w-0">
|
|
|
- <div className="flex items-center gap-1.5 mb-0.5">
|
|
|
- <span className="text-[11px] font-medium" style={{ color: 'var(--text)' }}>{comment.user?.name ?? 'Unknown'}</span>
|
|
|
- {comment.timestamp != null && (
|
|
|
- <span className="text-[10px] font-mono px-1 rounded" style={{ background: 'rgba(99,102,241,0.10)', color: '#818CF8' }}>
|
|
|
- {formatTimecode(comment.timestamp, fps, asset?.duration ?? 0)}
|
|
|
- </span>
|
|
|
- )}
|
|
|
- </div>
|
|
|
- <p className="text-[11px] truncate" style={{ color: 'var(--text-muted)' }}>{comment.content}</p>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- ))
|
|
|
- )}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* Compare video + its comments — only show when durations match */}
|
|
|
- {compareAsset && !compareMismatch && (
|
|
|
- <div className="flex-1 min-w-0 flex flex-col gap-0">
|
|
|
- <div className="text-xs mb-1 px-1 truncate" style={{ color: 'rgba(255,255,255,0.5)' }}>
|
|
|
- {compareAsset.title}
|
|
|
- </div>
|
|
|
+ <div className="flex-1 min-h-0 flex flex-col gap-0">
|
|
|
<VideoPlayer
|
|
|
- src={compareAsset.hlsPath ? `${API_BASE}/uploads${compareAsset.hlsPath}` : `${API_BASE}/uploads/${compareAsset.filePath}`}
|
|
|
- mimeType={compareAsset.mimeType}
|
|
|
- fps={compareAsset.fps ?? 30}
|
|
|
- comments={compareComments}
|
|
|
- visibleAnnotations={compareVisibleAnnotations}
|
|
|
- drawMode={false}
|
|
|
+ src={videoUrl}
|
|
|
+ mimeType={asset.mimeType}
|
|
|
+ fps={fps}
|
|
|
+ comments={showMainAnnotations ? allComments : []}
|
|
|
+ visibleAnnotations={showMainAnnotations ? visibleAnnotations : []}
|
|
|
+ drawMode={drawMode}
|
|
|
drawTool={drawTool}
|
|
|
drawColor={drawColor}
|
|
|
- onDrawModeChange={() => {}}
|
|
|
- onDrawToolChange={() => {}}
|
|
|
- onDrawColorChange={() => {}}
|
|
|
- pendingStrokes={[]}
|
|
|
- onStrokeComplete={() => {}}
|
|
|
- onTimeUpdate={() => {}}
|
|
|
- onCommentClick={() => {}}
|
|
|
- isComparePlayer={true}
|
|
|
+ onDrawModeChange={setDrawMode}
|
|
|
+ onDrawToolChange={setDrawTool}
|
|
|
+ onDrawColorChange={setDrawColor}
|
|
|
+ pendingStrokes={pendingStrokes}
|
|
|
+ onStrokeComplete={handleStrokeComplete}
|
|
|
+ onTimeUpdate={handleTimeUpdate}
|
|
|
+ onCommentClick={handleCommentSeek}
|
|
|
+ onPlayingChange={setPlaying}
|
|
|
+ onTimelineSeek={handleTimeUpdate}
|
|
|
externalCurrentTime={currentTime}
|
|
|
externalPlaying={playing}
|
|
|
/>
|
|
|
- {/* Comments below compare video */}
|
|
|
- <div className="mt-2 rounded-xl overflow-hidden" style={{ background: 'rgba(10,11,20,0.80)', border: '1px solid rgba(255,255,255,0.06)' }}>
|
|
|
- <div className="px-3 py-2 flex items-center gap-2" style={{ borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
|
|
|
+ {/* Comments below main video — full available height */}
|
|
|
+ <div className="mt-2 rounded-xl flex-1 min-h-0 flex flex-col overflow-hidden" style={{ background: 'rgba(10,11,20,0.80)', border: '1px solid rgba(255,255,255,0.06)' }}>
|
|
|
+ <div className="px-3 py-2 shrink-0 flex items-center gap-2" style={{ borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
|
|
|
<span className="text-xs font-medium" style={{ color: 'var(--text)' }}>
|
|
|
Comments
|
|
|
</span>
|
|
|
<span className="text-xs px-1.5 py-0.5 rounded-full" style={{ background: 'rgba(255,255,255,0.06)', color: 'var(--text-muted)' }}>
|
|
|
- {compareVisibleComments.length}
|
|
|
+ {visibleComments.length}
|
|
|
+ </span>
|
|
|
+ <span className="font-mono text-[11px] ml-auto" style={{ color: '#818CF8' }}>
|
|
|
+ {formatTimecode(currentTime, fps, asset?.duration ?? 0)}
|
|
|
</span>
|
|
|
</div>
|
|
|
- <div className="max-h-32 overflow-y-auto">
|
|
|
- {compareVisibleComments.length === 0 ? (
|
|
|
+ <div className="flex-1 overflow-y-auto scroll-area">
|
|
|
+ {visibleComments.length === 0 ? (
|
|
|
<p className="text-xs text-center py-4" style={{ color: 'var(--text-muted)' }}>No comments</p>
|
|
|
) : (
|
|
|
- compareVisibleComments.slice(0, 5).map(comment => (
|
|
|
- <div key={comment.id} className="px-3 py-2 flex items-start gap-2" style={{ borderBottom: '1px solid rgba(255,255,255,0.04)' }}>
|
|
|
+ visibleComments.map(comment => (
|
|
|
+ <div key={comment.id} className="px-3 py-2.5 flex items-start gap-2" style={{ borderBottom: '1px solid rgba(255,255,255,0.04)' }}>
|
|
|
<Avatar name={comment.user?.name ?? 'U'} size="xs" />
|
|
|
<div className="flex-1 min-w-0">
|
|
|
<div className="flex items-center gap-1.5 mb-0.5">
|
|
|
@@ -707,7 +666,7 @@ export default function ReviewPage() {
|
|
|
</span>
|
|
|
)}
|
|
|
</div>
|
|
|
- <p className="text-[11px] truncate" style={{ color: 'var(--text-muted)' }}>{comment.content}</p>
|
|
|
+ <p className="text-[11px] leading-relaxed" style={{ color: 'var(--text-muted)' }}>{comment.content}</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
))
|
|
|
@@ -715,6 +674,86 @@ export default function ReviewPage() {
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Compare video + its comments — only show when durations match */}
|
|
|
+ {compareAsset && !compareMismatch && (
|
|
|
+ <div className="flex-1 min-w-0 flex flex-col gap-0 min-h-0">
|
|
|
+ {/* Annotation toggle */}
|
|
|
+ <div className="flex items-center gap-2 mb-1 px-1">
|
|
|
+ <button
|
|
|
+ onClick={() => setShowCompareAnnotations(v => !v)}
|
|
|
+ className="flex items-center gap-1.5 text-[11px] px-2 py-1 rounded-md transition-colors"
|
|
|
+ style={showCompareAnnotations
|
|
|
+ ? { background: 'rgba(99,102,241,0.15)', color: '#818CF8' }
|
|
|
+ : { background: 'rgba(255,255,255,0.05)', color: 'var(--text-subtle)' }}
|
|
|
+ title={showCompareAnnotations ? 'Hide annotations' : 'Show annotations'}
|
|
|
+ >
|
|
|
+ <svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
|
+ <path strokeLinecap="round" strokeLinejoin="round" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
|
|
+ </svg>
|
|
|
+ Annot.
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <div className="text-xs mb-1 px-1 truncate" style={{ color: 'rgba(255,255,255,0.5)' }}>
|
|
|
+ {compareAsset.title}
|
|
|
+ </div>
|
|
|
+ <div className="flex-1 min-h-0 flex flex-col gap-0">
|
|
|
+ <VideoPlayer
|
|
|
+ src={compareAsset.hlsPath ? `${API_BASE}/uploads${compareAsset.hlsPath}` : `${API_BASE}/uploads/${compareAsset.filePath}`}
|
|
|
+ mimeType={compareAsset.mimeType}
|
|
|
+ fps={compareAsset.fps ?? 30}
|
|
|
+ comments={showCompareAnnotations ? compareComments : []}
|
|
|
+ visibleAnnotations={showCompareAnnotations ? compareVisibleAnnotations : []}
|
|
|
+ drawMode={false}
|
|
|
+ drawTool={drawTool}
|
|
|
+ drawColor={drawColor}
|
|
|
+ onDrawModeChange={() => {}}
|
|
|
+ onDrawToolChange={() => {}}
|
|
|
+ onDrawColorChange={() => {}}
|
|
|
+ pendingStrokes={[]}
|
|
|
+ onStrokeComplete={() => {}}
|
|
|
+ onTimeUpdate={() => {}}
|
|
|
+ onCommentClick={() => {}}
|
|
|
+ isComparePlayer={true}
|
|
|
+ externalCurrentTime={currentTime}
|
|
|
+ externalPlaying={playing}
|
|
|
+ />
|
|
|
+ {/* Comments below compare video — full available height */}
|
|
|
+ <div className="mt-2 rounded-xl flex-1 min-h-0 flex flex-col overflow-hidden" style={{ background: 'rgba(10,11,20,0.80)', border: '1px solid rgba(255,255,255,0.06)' }}>
|
|
|
+ <div className="px-3 py-2 shrink-0 flex items-center gap-2" style={{ borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
|
|
|
+ <span className="text-xs font-medium" style={{ color: 'var(--text)' }}>
|
|
|
+ Comments
|
|
|
+ </span>
|
|
|
+ <span className="text-xs px-1.5 py-0.5 rounded-full" style={{ background: 'rgba(255,255,255,0.06)', color: 'var(--text-muted)' }}>
|
|
|
+ {compareVisibleComments.length}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <div className="flex-1 overflow-y-auto scroll-area">
|
|
|
+ {compareVisibleComments.length === 0 ? (
|
|
|
+ <p className="text-xs text-center py-4" style={{ color: 'var(--text-muted)' }}>No comments</p>
|
|
|
+ ) : (
|
|
|
+ compareVisibleComments.map(comment => (
|
|
|
+ <div key={comment.id} className="px-3 py-2.5 flex items-start gap-2" style={{ borderBottom: '1px solid rgba(255,255,255,0.04)' }}>
|
|
|
+ <Avatar name={comment.user?.name ?? 'U'} size="xs" />
|
|
|
+ <div className="flex-1 min-w-0">
|
|
|
+ <div className="flex items-center gap-1.5 mb-0.5">
|
|
|
+ <span className="text-[11px] font-medium" style={{ color: 'var(--text)' }}>{comment.user?.name ?? 'Unknown'}</span>
|
|
|
+ {comment.timestamp != null && (
|
|
|
+ <span className="text-[10px] font-mono px-1 rounded" style={{ background: 'rgba(99,102,241,0.10)', color: '#818CF8' }}>
|
|
|
+ {formatTimecode(comment.timestamp, fps, asset?.duration ?? 0)}
|
|
|
+ </span>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ <p className="text-[11px] leading-relaxed" style={{ color: 'var(--text-muted)' }}>{comment.content}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ ))
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
)}
|
|
|
</div>
|
|
|
) : (
|