Skip to content

Commit b4e7f54

Browse files
committed
feat: whiteboard feature implemented
1 parent 9437717 commit b4e7f54

8 files changed

Lines changed: 586 additions & 27 deletions

File tree

README.md

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@
77
[![Next.js 15](https://img.shields.io/badge/Next.js-15.5.2-black?style=flat-square&logo=next.js)](https://nextjs.org/)
88
[![FastAPI](https://img.shields.io/badge/FastAPI-Latest-green?style=flat-square&logo=fastapi)](https://fastapi.tiangolo.com/)
99
[![Python 3.11+](https://img.shields.io/badge/Python-3.11+-blue?style=flat-square&logo=python)](https://python.org)
10-
[![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue?style=flat-square&logo=typescript)](https://typescriptlang.org)
11-
[![Ollama](https://img.shields.io/badge/Ollama-Gemma_270M-purple?style=flat-square)](https://ollama.ai)
10+
[![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue?style=flat-square&logo=typescript)](https://typescriptlang.org) [![Ollama](https://img.shields.io/badge/Ollama-Gemma_270M-purple?style=flat-square)](https://ollama.ai)
1211
[![WebRTC](https://img.shields.io/badge/WebRTC-Real_Time-orange?style=flat-square)](https://webrtc.org/)
1312

1413
**✨ Revolutionary EdTech Platform with AI-Powered Learning Assistance ✨**
@@ -284,16 +283,16 @@ const notes = await getGeneratedNotes(classId);
284283
- [x] Cross-browser compatibility
285284

286285
### Phase 2 - Enhanced Features (In Progress)
287-
- [ ] Voice transcription and live captions
288-
- [ ] Automated class notes generation
286+
- [x] Voice transcription and live captions
287+
- [x] Automated class notes generation
289288
- [ ] Advanced analytics dashboard
290289
- [ ] Enhanced mobile responsiveness
291-
- [ ] Performance optimization
290+
- [x] Performance optimization
292291

293292
### Phase 3 - Advanced Capabilities (Planned)
294-
- [ ] Interactive whiteboard collaboration
293+
- [x] Interactive whiteboard collaboration
295294
- [ ] Breakout room functionality
296-
- [ ] Multi-language support
295+
- [x] Multi-language support
297296
- [ ] Learning analytics and insights
298297
- [ ] Mobile application (React Native)
299298
- [ ] Integration with LMS platforms
@@ -373,21 +372,6 @@ git push origin feature/amazing-feature
373372

374373
---
375374

376-
## 🏆 **Awards & Recognition**
377-
378-
<div align="center">
379-
380-
![Trophy](https://img.shields.io/badge/🏆-Innovation_Award-gold?style=for-the-badge)
381-
![Star](https://img.shields.io/badge/⭐-Best_EdTech_2024-yellow?style=for-the-badge)
382-
![Medal](https://img.shields.io/badge/🥇-AI_Excellence-silver?style=for-the-badge)
383-
384-
*"Instructify represents the future of AI-powered education"*
385-
*- EdTech Innovation Awards 2024*
386-
387-
</div>
388-
389-
---
390-
391375
## 📄 **License**
392376

393377
<div align="center">
@@ -425,4 +409,4 @@ If you find Instructify useful, please give it a ⭐ on GitHub!
425409

426410
<div align="center">
427411
<sub><sup>© 2024 Instructify. All rights reserved. | Built for the future of education 🎓</sup></sub>
428-
</div>
412+
</div>

backend/app/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ async def websocket_endpoint(websocket: WebSocket, class_id: str):
141141
while True:
142142
data = await websocket.receive_text()
143143
message = json.loads(data)
144+
145+
print(f"📨 Received WebSocket message: {message.get('type')} from {user_name} ({user_type})")
144146

145147
await chat_handler.handle_message(class_id, websocket, message)
146148

backend/app/websockets/chat_handler.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ async def handle_message(
3434
await self.handle_ai_query(class_id, websocket, message_data)
3535
elif message_type == "webrtc_signal":
3636
await self.handle_webrtc_signal(class_id, websocket, message_data)
37+
elif message_type == "whiteboard_draw":
38+
await self.handle_whiteboard_draw(class_id, websocket, message_data)
39+
elif message_type == "test_message":
40+
print(f"🧪 Test message received: {message_data.get('data')}")
41+
else:
42+
print(f"❓ Unknown message type: {message_type}")
3743

3844
async def handle_chat_message(
3945
self, class_id: str, websocket: WebSocket, message_data: dict
@@ -230,6 +236,38 @@ async def handle_webrtc_signal(
230236
# Use the new WebRTC signaling handler
231237
await self.room_manager.handle_webrtc_signaling(class_id, websocket, message_data)
232238

239+
async def handle_whiteboard_draw(
240+
self, class_id: str, websocket: WebSocket, message_data: dict
241+
):
242+
"""Handle whiteboard drawing data"""
243+
if not self.room_manager:
244+
return
245+
246+
sender_info = self.room_manager.user_connections.get(websocket, {})
247+
sender_type = sender_info.get("user_type", "student")
248+
sender_name = sender_info.get("user_name", "Anonymous")
249+
250+
print(f"🎨 Whiteboard draw from {sender_name} ({sender_type})")
251+
252+
# Only teachers can draw on whiteboard
253+
if sender_type != "teacher":
254+
print(f"❌ Non-teacher {sender_name} tried to draw on whiteboard")
255+
return
256+
257+
drawing_data = message_data.get("drawing_data", {})
258+
print(f"📝 Broadcasting drawing data: {drawing_data}")
259+
260+
# Broadcast drawing data to all students
261+
await self.room_manager.broadcast_to_students(
262+
class_id,
263+
{
264+
"type": "whiteboard_update",
265+
"drawing_data": drawing_data,
266+
"timestamp": datetime.now().isoformat()
267+
}
268+
)
269+
print(f"✅ Whiteboard update sent to students in class {class_id}")
270+
233271
def get_chat_history(self, class_id: str) -> List[dict]:
234272
"""Get chat history for a classroom"""
235273
if class_id not in self.messages:

backend/app/websockets/room_manager.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,33 @@ async def broadcast_to_room(
122122
for conn in disconnected:
123123
await self.remove_user_from_room(class_id, conn)
124124

125+
async def broadcast_to_students(self, class_id: str, message: dict):
126+
"""Send message only to students in the room"""
127+
if class_id not in self.connections:
128+
print(f"❌ No connections found for class {class_id}")
129+
return
130+
131+
message_str = json.dumps(message)
132+
disconnected = []
133+
student_count = 0
134+
135+
for connection in self.connections[class_id]:
136+
user_info = self.user_connections.get(connection, {})
137+
if user_info.get("user_type") == "student":
138+
student_count += 1
139+
try:
140+
await connection.send_text(message_str)
141+
print(f"✅ Sent whiteboard update to student: {user_info.get('user_name', 'Unknown')}")
142+
except Exception as e:
143+
print(f"❌ Failed to send to student: {e}")
144+
disconnected.append(connection)
145+
146+
print(f"📊 Broadcast to {student_count} students in class {class_id}")
147+
148+
# Clean up disconnected connections
149+
for conn in disconnected:
150+
await self.remove_user_from_room(class_id, conn)
151+
125152
async def send_to_teachers(self, class_id: str, message: dict):
126153
"""Send message only to teachers in the room"""
127154
if class_id not in self.connections:

frontend/app/classroom/[id]/ClassroomContent.tsx

Lines changed: 111 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useState, useEffect, useRef, useCallback } from 'react';
44
import { useSearchParams } from 'next/navigation';
55
import VoiceTranscription from '../../components/VoiceTranscription';
66
import LiveCaptions from '../../components/LiveCaptions';
7+
import Whiteboard from '../../components/Whiteboard';
78

89
interface Message {
910
id: string;
@@ -64,6 +65,52 @@ export default function ClassroomContent({ classId }: { classId: string }) {
6465
finalText: '',
6566
isActive: false
6667
});
68+
69+
// Whiteboard state
70+
const [whiteboardVisible, setWhiteboardVisible] = useState(false);
71+
72+
// Whiteboard drawing function - always available
73+
const applyWhiteboardUpdate = useCallback((data: any) => {
74+
const canvas = document.querySelector('canvas');
75+
if (!canvas) {
76+
console.log('❌ No canvas found for whiteboard update');
77+
return;
78+
}
79+
80+
const ctx = canvas.getContext('2d');
81+
if (!ctx) return;
82+
83+
if (data.action === 'clear') {
84+
ctx.clearRect(0, 0, canvas.width, canvas.height);
85+
console.log('✅ Cleared whiteboard canvas');
86+
return;
87+
}
88+
89+
if (data.tool === 'eraser') {
90+
ctx.globalCompositeOperation = 'destination-out';
91+
} else {
92+
ctx.globalCompositeOperation = 'source-over';
93+
ctx.strokeStyle = data.color || '#ffffff';
94+
}
95+
96+
ctx.lineWidth = data.size;
97+
ctx.lineTo(data.x, data.y);
98+
ctx.stroke();
99+
ctx.beginPath();
100+
ctx.moveTo(data.x, data.y);
101+
102+
console.log('✅ Applied whiteboard drawing:', data);
103+
}, []);
104+
105+
// Register whiteboard function globally
106+
useEffect(() => {
107+
(window as any).applyWhiteboardUpdate = applyWhiteboardUpdate;
108+
console.log('✅ Whiteboard function registered at parent level');
109+
110+
return () => {
111+
(window as any).applyWhiteboardUpdate = null;
112+
};
113+
}, [applyWhiteboardUpdate]);
67114

68115
const wsRef = useRef<WebSocket | null>(null);
69116
const videoRef = useRef<HTMLVideoElement>(null);
@@ -164,6 +211,16 @@ export default function ClassroomContent({ classId }: { classId: string }) {
164211
// Show warning to user that their message was blocked
165212
alert(`⚠️ Message Blocked: ${data.reason}`);
166213
break;
214+
case 'whiteboard_update':
215+
// Apply whiteboard drawing update
216+
console.log('📝 Student received whiteboard update:', data.drawing_data);
217+
if ((window as any).applyWhiteboardUpdate) {
218+
(window as any).applyWhiteboardUpdate(data.drawing_data);
219+
console.log('✅ Applied whiteboard update to canvas');
220+
} else {
221+
console.log('❌ applyWhiteboardUpdate function not available');
222+
}
223+
break;
167224
}
168225
};
169226

@@ -481,10 +538,15 @@ export default function ClassroomContent({ classId }: { classId: string }) {
481538

482539
if (useScreen && mediaState.canShareScreen) {
483540
console.log('📺 Starting screen share...');
484-
// Screen sharing
541+
// Screen sharing with Chrome compatibility
485542
stream = await navigator.mediaDevices.getDisplayMedia({
486-
video: true,
487-
audio: mediaState.hasAudio
543+
video: {
544+
mediaSource: 'screen',
545+
width: { max: 1920 },
546+
height: { max: 1080 },
547+
frameRate: { max: 30 }
548+
},
549+
audio: false
488550
});
489551
setMediaState(prev => ({ ...prev, isScreenSharing: true }));
490552
} else {
@@ -656,6 +718,24 @@ export default function ClassroomContent({ classId }: { classId: string }) {
656718
setCaptionState({ currentText, finalText, isActive });
657719
}, []);
658720

721+
// Handle whiteboard drawing updates
722+
const handleWhiteboardUpdate = useCallback((drawingData: any) => {
723+
console.log('📝 Sending whiteboard update:', drawingData);
724+
if (wsRef.current && role === 'teacher') {
725+
// Send test message first
726+
wsRef.current.send(JSON.stringify({
727+
type: 'test_message',
728+
data: 'Testing WebSocket connection'
729+
}));
730+
731+
// Then send whiteboard data
732+
wsRef.current.send(JSON.stringify({
733+
type: 'whiteboard_draw',
734+
drawing_data: drawingData
735+
}));
736+
}
737+
}, [role]);
738+
659739
if (!isConnected) {
660740
return (
661741
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
@@ -755,6 +835,13 @@ export default function ClassroomContent({ classId }: { classId: string }) {
755835
currentText={captionState.currentText}
756836
finalText={captionState.finalText}
757837
/>
838+
839+
{/* Whiteboard Overlay */}
840+
<Whiteboard
841+
isVisible={whiteboardVisible}
842+
isTeacher={role === 'teacher'}
843+
onDrawingUpdate={handleWhiteboardUpdate}
844+
/>
758845
</div>
759846

760847
{/* Professional Media Controls for Teacher */}
@@ -836,7 +923,7 @@ export default function ClassroomContent({ classId }: { classId: string }) {
836923
</div>
837924
</div>
838925

839-
{/* Center Section - Screen Share */}
926+
{/* Center Section - Screen Share & Whiteboard */}
840927
<div className="flex items-center space-x-3">
841928
<div className="relative group">
842929
<button
@@ -860,6 +947,26 @@ export default function ClassroomContent({ classId }: { classId: string }) {
860947
)}
861948
</button>
862949
</div>
950+
951+
<div className="relative group">
952+
<button
953+
onClick={() => setWhiteboardVisible(!whiteboardVisible)}
954+
className={`px-6 py-3 rounded-lg transition-all duration-200 flex items-center space-x-2 font-medium ${
955+
whiteboardVisible
956+
? 'bg-purple-500 hover:bg-purple-600 text-white shadow-lg shadow-purple-500/25'
957+
: 'bg-gray-700 hover:bg-gray-600 text-white shadow-lg'
958+
}`}
959+
title={whiteboardVisible ? 'Hide whiteboard' : 'Show whiteboard'}
960+
>
961+
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
962+
<path d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1v-6zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z" />
963+
</svg>
964+
<span>🎨 Whiteboard</span>
965+
{whiteboardVisible && (
966+
<div className="w-2 h-2 bg-purple-400 rounded-full animate-pulse"></div>
967+
)}
968+
</button>
969+
</div>
863970
</div>
864971

865972
{/* Right Section - End Session */}

0 commit comments

Comments
 (0)