Conversation
❌ Deploy Preview for ornate-blancmange-89db6b failed. Why did it fail? →
|
Summary of ChangesHello @aklry, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! 此拉取请求的核心目标是为应用程序引入一个全新的实时视频会议模块。通过集成 LiveKit 平台,用户现在可以无缝地创建和加入视频会议,享受包括音视频通信、屏幕共享和端到端加密在内的丰富功能。此次更新不仅带来了全新的会议页面和交互组件,还着重于提升用户体验,例如提供会议前设置、便捷的房间分享方式,并内置了性能优化和详细的调试工具,确保会议的流畅性和稳定性。 Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
此 PR 新增了基于 LiveKit 的视频会议功能。主要变更包括:添加了 /rooms 路由,包含创建/加入房间的大厅页面和视频会议房间页面;引入了 LiveKit 相关的依赖,并添加了大量新的 React 组件、Hooks 和 API 服务来支持会议功能,例如会前准备界面、自定义控制条、性能优化钩子等;更新了 package.json 和 pnpm-lock.yaml 以包含新依赖,并添加了 renovate.json 来管理依赖更新;在仪表盘页面添加了进入会议功能的快捷入口。整体来看,这是一个结构清晰、功能完整的新特性实现。我的审查意见主要集中在以下几个方面:
- 依赖管理:建议调整
renovate.json配置,对核心库的自动合并策略采取更谨慎的态度。 - 代码健壮性:指出了一个在
PageClientImpl.tsx中可能导致Room配置不更新的useMemo使用问题。 - 用户体验:提出了改进离开房间后的重定向逻辑,以及优化复制操作的用户反馈机制,避免使用
alert。 - 代码可维护性:建议将组件中大量的内联样式重构为 CSS Modules,以提高代码的可读性和可维护性。
这些修改将有助于提升新功能的稳定性和长期可维护性。
| }; | ||
| }, [props.options.hq, props.options.codec, e2eeEnabled, keyProvider, worker]); | ||
|
|
||
| const room = React.useMemo(() => new Room(roomOptions), []); |
There was a problem hiding this comment.
React.useMemo 的依赖项数组为空,导致 room 对象仅在组件首次挂载时创建一次。然而,roomOptions 自身是 memoized 的,并且其依赖项 [props.options.hq, props.options.codec, e2eeEnabled, keyProvider, worker] 可能会在组件生命周期内发生变化。当 roomOptions 变化时,Room 实例不会被重新创建,这会导致新的配置(如视频质量 hq 或编解码器 codec)无法生效。
建议将 roomOptions 添加到 useMemo 的依赖项数组中,以确保在配置变更时能重新创建 Room 实例。
| const room = React.useMemo(() => new Room(roomOptions), []); | |
| const room = React.useMemo(() => new Room(roomOptions), [roomOptions]); |
| const [copied] = React.useState(false); | ||
|
|
||
| const shareUrl = | ||
| typeof window !== 'undefined' ? `${window.location.origin}?join=${roomName}` : ''; | ||
|
|
||
| // 快速复制房间号(点击按钮时) | ||
| // const handleQuickCopy = () => { | ||
| // navigator.clipboard.writeText(roomName).then(() => { | ||
| // setCopied(true); | ||
| // setTimeout(() => setCopied(false), 2000); | ||
| // }); | ||
| // }; | ||
|
|
||
| const handleCopyRoomId = () => { | ||
| navigator.clipboard.writeText(roomName).then(() => { | ||
| alert('✅ 房间号已复制:' + roomName); | ||
| }); | ||
| }; | ||
|
|
||
| const copyShareLink = () => { | ||
| navigator.clipboard.writeText(shareUrl).then(() => { | ||
| alert('✅ 分享链接已复制!'); | ||
| }); | ||
| }; |
There was a problem hiding this comment.
copied 状态被声明为常量,无法更新,导致“复制房间号”按钮在点击后无法提供正确的 UI 反馈。此外,使用 alert 会中断用户流程,体验不佳。
建议重构复制逻辑:
- 将
copied状态声明修改为const [copied, setCopied] = React.useState(false);。 - 修改
handleCopyRoomId和copyShareLink函数,在复制成功后更新状态,并使用setTimeout在短暂延迟后重置状态。 - 移除
alert,让 UI 变化(如改变按钮文本从“复制”变为“✓ 已复制”)作为反馈,以提升用户体验。
export function ShareRoomButton({ roomName, onClose }: ShareRoomButtonProps) {
const [copied, setCopied] = React.useState(false);
const shareUrl =
typeof window !== 'undefined' ? `${window.location.origin}?join=${roomName}` : '';
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
});
};
const handleCopyRoomId = () => {
copyToClipboard(roomName);
};
const copyShareLink = () => {
copyToClipboard(shareUrl);
};
| "matchSourceUrlPrefixes": ["https://github.com/livekit/"], | ||
| "rangeStrategy": "replace", | ||
| "groupName": "LiveKit dependencies (non-major)", | ||
| "automerge": true | ||
| } |
| console.log('🖼️ 设置用户头像:', props.connectionDetails.userAvatar); | ||
| room.localParticipant.setMetadata( | ||
| JSON.stringify({ | ||
| avatar: '/favicon.svg', | ||
| }), | ||
| ); |
There was a problem hiding this comment.
| ); | ||
|
|
||
| const router = useRouter(); | ||
| const handleOnLeave = React.useCallback(() => router.push('/'), [router]); |
| <div | ||
| style={{ | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| minHeight: '100vh', | ||
| padding: '2rem', | ||
| background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', | ||
| }} | ||
| > | ||
| <div | ||
| style={{ | ||
| background: 'rgba(255, 255, 255, 0.98)', | ||
| borderRadius: '20px', | ||
| padding: '3rem', | ||
| maxWidth: '480px', | ||
| width: '100%', | ||
| boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)', | ||
| }} | ||
| > | ||
| <h1 | ||
| style={{ | ||
| fontSize: '2rem', | ||
| fontWeight: '700', | ||
| color: '#1a1a1a', | ||
| marginBottom: '0.5rem', | ||
| textAlign: 'center', | ||
| }} | ||
| > | ||
| Join Room | ||
| </h1> | ||
|
|
||
| <p | ||
| style={{ | ||
| color: '#666', | ||
| textAlign: 'center', | ||
| marginBottom: '1rem', | ||
| fontSize: '0.95rem', | ||
| }} | ||
| > | ||
| Room: <strong>{roomName}</strong> | ||
| </p> | ||
|
|
||
| <p | ||
| style={{ | ||
| color: '#888', | ||
| textAlign: 'center', | ||
| marginBottom: '1.5rem', | ||
| fontSize: '0.85rem', | ||
| lineHeight: '1.4', | ||
| }} | ||
| > | ||
| 💡 提示:即使没有摄像头也可以加入,只用麦克风聊天 | ||
| </p> | ||
|
|
||
| <form onSubmit={handleSubmit}> | ||
| <div style={{ marginBottom: '1.5rem' }}> | ||
| <label | ||
| htmlFor="username" | ||
| style={{ | ||
| display: 'block', | ||
| marginBottom: '0.5rem', | ||
| color: '#374151', | ||
| fontSize: '0.875rem', | ||
| fontWeight: '600', | ||
| }} | ||
| > | ||
| Your Name * | ||
| </label> | ||
| <input | ||
| id="username" | ||
| type="text" | ||
| placeholder="Enter your name" | ||
| value={username} | ||
| onChange={(e) => setUsername(e.target.value)} | ||
| disabled={isJoining} | ||
| autoFocus | ||
| style={{ | ||
| width: '100%', | ||
| padding: '0.875rem 1rem', | ||
| border: '2px solid #e5e7eb', | ||
| borderRadius: '12px', | ||
| fontSize: '1rem', | ||
| transition: 'border-color 0.2s', | ||
| outline: 'none', | ||
| }} | ||
| onFocus={(e) => (e.target.style.borderColor = '#667eea')} | ||
| onBlur={(e) => (e.target.style.borderColor = '#e5e7eb')} | ||
| required | ||
| /> | ||
| </div> | ||
|
|
||
| <div | ||
| style={{ | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| gap: '1rem', | ||
| marginBottom: '2rem', | ||
| }} | ||
| > | ||
| <label | ||
| style={{ | ||
| display: 'flex', | ||
| alignItems: 'center', | ||
| gap: '0.75rem', | ||
| cursor: 'pointer', | ||
| padding: '0.75rem', | ||
| background: videoEnabled ? 'rgba(102, 126, 234, 0.1)' : 'rgba(0, 0, 0, 0.05)', | ||
| borderRadius: '12px', | ||
| transition: 'background 0.2s', | ||
| }} | ||
| > | ||
| <input | ||
| type="checkbox" | ||
| checked={videoEnabled} | ||
| onChange={(e) => setVideoEnabled(e.target.checked)} | ||
| disabled={isJoining} | ||
| style={{ | ||
| width: '20px', | ||
| height: '20px', | ||
| cursor: 'pointer', | ||
| }} | ||
| /> | ||
| <span | ||
| style={{ | ||
| fontSize: '0.95rem', | ||
| fontWeight: '500', | ||
| color: '#1a1a1a', | ||
| }} | ||
| > | ||
| 📹 Enable Camera | ||
| </span> | ||
| </label> | ||
|
|
||
| <label | ||
| style={{ | ||
| display: 'flex', | ||
| alignItems: 'center', | ||
| gap: '0.75rem', | ||
| cursor: 'pointer', | ||
| padding: '0.75rem', | ||
| background: audioEnabled ? 'rgba(102, 126, 234, 0.1)' : 'rgba(0, 0, 0, 0.05)', | ||
| borderRadius: '12px', | ||
| transition: 'background 0.2s', | ||
| }} | ||
| > | ||
| <input | ||
| type="checkbox" | ||
| checked={audioEnabled} | ||
| onChange={(e) => setAudioEnabled(e.target.checked)} | ||
| disabled={isJoining} | ||
| style={{ | ||
| width: '20px', | ||
| height: '20px', | ||
| cursor: 'pointer', | ||
| }} | ||
| /> | ||
| <span | ||
| style={{ | ||
| fontSize: '0.95rem', | ||
| fontWeight: '500', | ||
| color: '#1a1a1a', | ||
| }} | ||
| > | ||
| 🎤 Enable Microphone | ||
| </span> | ||
| </label> | ||
| </div> | ||
|
|
||
| <button | ||
| type="submit" | ||
| disabled={isJoining || !username.trim()} | ||
| style={{ | ||
| width: '100%', | ||
| padding: '1rem', | ||
| background: | ||
| isJoining || !username.trim() | ||
| ? '#e5e7eb' | ||
| : 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', | ||
| color: 'white', | ||
| border: 'none', | ||
| borderRadius: '12px', | ||
| fontSize: '1rem', | ||
| fontWeight: '600', | ||
| cursor: isJoining || !username.trim() ? 'not-allowed' : 'pointer', | ||
| transition: 'all 0.2s', | ||
| boxShadow: | ||
| isJoining || !username.trim() ? 'none' : '0 4px 15px rgba(102, 126, 234, 0.4)', | ||
| }} | ||
| > | ||
| {isJoining ? 'Joining...' : 'Join Room'} | ||
| </button> | ||
| </form> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
| style={{ | ||
| width: '100%', | ||
| padding: '1.25rem', | ||
| fontSize: '1.1rem', | ||
| fontWeight: '600', | ||
| background: isCreating | ||
| ? 'rgba(102, 126, 234, 0.5)' | ||
| : 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', | ||
| border: 'none', | ||
| borderRadius: '8px', | ||
| cursor: isCreating ? 'not-allowed' : 'pointer', | ||
| transition: 'all 0.2s', | ||
| opacity: isCreating ? 0.7 : 1, | ||
| }} |
PR 描述
新增视频会议页面
PR 类型
Issue 关联
Closes #
其他信息