IntegrationTest / src /components /FocusPage.jsx
Yingtao-Zheng's picture
Upload partially updated files
8bbb872
import React, { useState, useEffect } from 'react';
function FocusPage({ videoManager, sessionResult, setSessionResult, isActive, displayVideoRef }) {
const [currentFrame, setCurrentFrame] = useState(30);
const [timelineEvents, setTimelineEvents] = useState([]);
const videoRef = displayVideoRef;
// 辅助函数:格式化时间
const formatDuration = (seconds) => {
// 如果是 0,直接显示 0s (或者你可以保留原来的 0m 0s)
if (seconds === 0) return "0s";
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}m ${secs}s`;
};
useEffect(() => {
if (!videoManager) return;
// 设置回调函数来更新时间轴
const originalOnStatusUpdate = videoManager.callbacks.onStatusUpdate;
videoManager.callbacks.onStatusUpdate = (isFocused) => {
setTimelineEvents(prev => {
const newEvents = [...prev, { isFocused, timestamp: Date.now() }];
if (newEvents.length > 60) newEvents.shift();
return newEvents;
});
// 调用原始回调(如果有)
if (originalOnStatusUpdate) originalOnStatusUpdate(isFocused);
};
// 清理函数:不再自动停止session,只清理回调
return () => {
if (videoManager) {
videoManager.callbacks.onStatusUpdate = originalOnStatusUpdate;
}
};
}, [videoManager]);
const handleStart = async () => {
try {
if (videoManager) {
setSessionResult(null); // 开始时清除结果层
setTimelineEvents([]);
console.log('🎬 Initializing camera...');
await videoManager.initCamera(videoRef.current);
console.log('✅ Camera initialized');
console.log('🚀 Starting streaming...');
await videoManager.startStreaming();
console.log('✅ Streaming started successfully');
}
} catch (err) {
console.error('❌ Start error:', err);
let errorMessage = "Failed to start: ";
if (err.name === 'NotAllowedError') {
errorMessage += "Camera permission denied. Please allow camera access.";
} else if (err.name === 'NotFoundError') {
errorMessage += "No camera found. Please connect a camera.";
} else if (err.name === 'NotReadableError') {
errorMessage += "Camera is already in use by another application.";
} else if (err.message && err.message.includes('HTTPS')) {
errorMessage += "Camera requires HTTPS. Please use a secure connection.";
} else {
errorMessage += err.message || "Unknown error occurred.";
}
alert(errorMessage + "\n\nCheck browser console for details.");
}
};
const handleStop = () => {
if (videoManager) {
videoManager.stopStreaming();
}
};
const handlePiP = async () => {
try {
const sourceVideoEl = videoRef.current;
if (!sourceVideoEl) {
alert('Video not ready. Please click Start first.');
return;
}
if (document.pictureInPictureElement) {
await document.exitPictureInPicture();
return;
}
sourceVideoEl.disablePictureInPicture = false;
if (typeof sourceVideoEl.webkitSetPresentationMode === 'function') {
sourceVideoEl.play().catch(() => {});
sourceVideoEl.webkitSetPresentationMode('picture-in-picture');
return;
}
if (!document.pictureInPictureEnabled || typeof sourceVideoEl.requestPictureInPicture !== 'function') {
alert('Picture-in-Picture is not supported in this browser.');
return;
}
const pipPromise = sourceVideoEl.requestPictureInPicture();
sourceVideoEl.play().catch(() => {});
await pipPromise;
} catch (err) {
console.error('PiP error:', err);
alert('Failed to enter Picture-in-Picture.');
}
};
// 浮窗功能
const handleFloatingWindow = () => {
handlePiP();
};
// ==========================================
// 新增功能:预览按钮的处理函数
// ==========================================
const handlePreview = () => {
// 强制设置一个 0 分 0 秒的假数据,触发 overlay 显示
setSessionResult({
duration_seconds: 0,
focus_score: 0
});
};
const handleCloseOverlay = () => {
setSessionResult(null);
};
// ==========================================
const handleFrameChange = (val) => {
setCurrentFrame(val);
if (videoManager) {
videoManager.setFrameRate(val);
}
};
const pageStyle = isActive
? undefined
: {
position: 'absolute',
width: '1px',
height: '1px',
overflow: 'hidden',
opacity: 0,
pointerEvents: 'none'
};
return (
<main id="page-b" className="page" style={pageStyle}>
{/* 1. Camera / Display Area */}
<section id="display-area" style={{ position: 'relative', overflow: 'hidden' }}>
<video
ref={videoRef}
muted
playsInline
autoPlay
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
/>
{/* 结果覆盖层 */}
{sessionResult && (
<div className="session-result-overlay">
<h3>Session Complete!</h3>
<div className="result-item">
<span className="label">Duration:</span>
<span className="value">{formatDuration(sessionResult.duration_seconds)}</span>
</div>
<div className="result-item">
<span className="label">Focus Score:</span>
<span className="value">{(sessionResult.focus_score * 100).toFixed(1)}%</span>
</div>
{/* 新增:加一个小按钮方便关闭预览 */}
<button
onClick={handleCloseOverlay}
style={{
marginTop: '20px',
padding: '8px 20px',
background: 'transparent',
border: '1px solid white',
color: 'white',
borderRadius: '20px',
cursor: 'pointer'
}}
>
Close
</button>
</div>
)}
</section>
{/* 2. Timeline Area */}
<section id="timeline-area">
<div className="timeline-label">Timeline</div>
<div id="timeline-visuals">
{timelineEvents.map((event, index) => (
<div
key={index}
className="timeline-block"
style={{
backgroundColor: event.isFocused ? '#00FF00' : '#FF0000',
width: '10px',
height: '20px',
display: 'inline-block',
marginRight: '2px',
borderRadius: '2px'
}}
title={event.isFocused ? 'Focused' : 'Distracted'}
/>
))}
</div>
<div id="timeline-line"></div>
</section>
{/* 3. Control Buttons */}
<section id="control-panel">
<button id="btn-cam-start" className="action-btn green" onClick={handleStart}>Start</button>
<button id="btn-floating" className="action-btn yellow" onClick={handleFloatingWindow}>Floating Window</button>
{/* 修改:把 Models 按钮暂时改成 Preview 按钮,或者加在它后面 */}
<button
id="btn-preview"
className="action-btn"
style={{ backgroundColor: '#6c5ce7' }} // 紫色按钮以示区别
onClick={handlePreview}
>
Preview Result
</button>
<button id="btn-cam-stop" className="action-btn red" onClick={handleStop}>Stop</button>
</section>
{/* 4. Frame Control */}
<section id="frame-control">
<label htmlFor="frame-slider">Frame</label>
<input
type="range"
id="frame-slider"
min="1"
max="60"
value={currentFrame}
onChange={(e) => handleFrameChange(e.target.value)}
/>
<input
type="number"
id="frame-input"
value={currentFrame}
onChange={(e) => handleFrameChange(e.target.value)}
/>
</section>
</main>
);
}
export default FocusPage;