feat: Complete tablet UI overhaul with popup menu system and help bubble

Major Changes:
- 🪟 Popup Menu System: Converted slide-down navigation to draggable, minimizable popup window
- ❓ Help Bubble: Added contextual help system for admin dashboard (accessible via ? bubble)
- 🎨 Universal Theme Engine: All elements now properly match selected theme across all panels
- 📱 Enhanced Tablet Support: Comprehensive touch gestures, handwriting recognition, and Apple Pencil optimization
- 🖊️ Unified Write Mode: Combined handwriting and keyboard input with large canvas interface

Technical Implementation:
- Draggable popup with minimize/restore functionality and boundary constraints
- Dynamic theme switching using CSS variables throughout all components
- Help system with modal overlay and keyboard shortcuts (ESC to close)
- Responsive design optimizations for mobile, tablet, and desktop
- Comprehensive CSS overrides to eliminate hardcoded colors
- JavaScript fixes for canvas stroke colors and input styling

UI/UX Improvements:
- Clean admin dashboard without instruction clutter
- Professional popup interface with smooth animations
- Better contrast and readability across all themes (default, playground, chalkboard, galaxy, nature)
- Improved accessibility with proper focus management
- Enhanced mobile responsiveness with touch-optimized controls

Files Modified:
- Menu system: tablet-menu.js, tablet-menu.css
- Theme engine: styles.css, themes integration
- Admin interface: index.html, ui.js
- Input systems: game.js, unified-write.js, handwriting.js
- Authentication: auth.js with tablet menu cleanup
This commit is contained in:
nmemmert committed 2025-10-02 14:26:04 -04:00
1 parent 0b9d46fed1
commit 8d37038177
24 files changed
+9506 -126

No files matched your search

+8
View File
@@ -72,6 +72,14 @@ async function verifyLogin() {
function logoutUser() {
localStorage.removeItem('loggedInUser');
// Clear current user
window.currentUser = null;
// Close tablet menu if open
if (window.tabletMenuManager && window.tabletMenuManager.isMenuOpen()) {
window.tabletMenuManager.forceCloseMenu();
}
// Hide all panels
document.getElementById('adminPanel')?.classList.add('hidden');
document.getElementById('studentPanel')?.classList.add('hidden');
+1 -1
View File
@@ -62,7 +62,7 @@ const bibleVerses = [
{
reference: "Matthew 6:33",
text: "But seek first the kingdom of God and his righteousness, and all these things will be added to you."
}
},
{
reference: "John 11:36",
text: "Jesus wept."
+105 -19
View File
@@ -7,13 +7,49 @@ let typingWords = [];
let isSpacedRepetitionMode = false;
// 📝 Check user's answer and move to next word regardless of correctness
window.submitAnswer = function() {
const input = document.getElementById('userInput').value.trim();
window.submitAnswer = function(providedAnswer = null) {
// Get input from unified write mode or legacy inputs
let input = '';
// First check if answer was provided directly (from unified write mode)
if (providedAnswer) {
input = providedAnswer.trim();
} else if (window.unifiedWriteMode) {
// Get from unified write mode
input = window.unifiedWriteMode.getText();
if (!input) {
alert('Please enter an answer first!');
return;
}
} else {
// Legacy fallback
const typingInput = document.getElementById('userInput');
const recognizedText = document.getElementById('recognizedText');
const handwritingContainer = document.getElementById('handwritingContainer');
// Check which input mode is active
if (handwritingContainer && handwritingContainer.style.display !== 'none') {
// Handwriting mode - get recognized text
input = recognizedText ? recognizedText.textContent.trim() : '';
if (input === 'Draw to see recognized text here' || input === '') {
alert('Please write something or switch to typing mode');
return;
}
} else {
// Typing mode - get typed input
input = typingInput.value.trim();
if (input === '') {
alert('Please type something or switch to handwriting mode');
return;
}
}
}
// Record the attempt
results.push({
word: currentWord,
attempt: input
attempt: input,
inputMode: handwritingContainer && handwritingContainer.style.display !== 'none' ? 'handwriting' : 'typing'
});
// Show feedback and move to next word regardless of correctness
@@ -21,7 +57,7 @@ window.submitAnswer = function() {
// Correct answer - add visual feedback
const inputEl = document.getElementById('userInput');
inputEl.style.borderColor = 'var(--success)';
inputEl.style.backgroundColor = '#f0fdf4';
inputEl.style.backgroundColor = 'var(--bg-secondary)';
// Add feedback text if element exists
const feedbackEl = document.getElementById('wordFeedback');
@@ -34,7 +70,7 @@ window.submitAnswer = function() {
// Incorrect answer - show correct spelling but continue
const inputEl = document.getElementById('userInput');
inputEl.style.borderColor = 'var(--error)';
inputEl.style.backgroundColor = '#fef2f2';
inputEl.style.backgroundColor = 'var(--bg-secondary)';
// Add feedback text if element exists
const feedbackEl = document.getElementById('wordFeedback');
@@ -63,6 +99,15 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
}
// Add keyboard support for unified write mode
if (window.unifiedWriteMode) {
document.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && document.activeElement === window.unifiedWriteMode.canvas) {
submitAnswer();
}
});
}
});
let currentSentence = '';
@@ -205,23 +250,64 @@ function showWord() {
inputSection.classList.remove('hidden');
inputSection.style.display = 'block';
// Reset and focus the input field with enhanced visibility
const userInput = document.getElementById('userInput');
userInput.value = '';
// Reset input (unified write mode or legacy)
if (window.unifiedWriteMode) {
window.unifiedWriteMode.clearText();
} else {
// Legacy input reset
const userInput = document.getElementById('userInput');
if (userInput) {
userInput.value = '';
userInput.classList.add('visible-input');
}
// Reset handwriting input
if (window.clearHandwriting) {
window.clearHandwriting();
}
// Reset recognition status and text
const recognitionStatus = document.getElementById('recognitionStatus');
const recognizedText = document.getElementById('recognizedText');
if (recognitionStatus) {
recognitionStatus.className = 'recognition-status empty';
recognitionStatus.textContent = '';
}
if (recognizedText) {
recognizedText.textContent = 'Draw to see recognized text here';
recognizedText.className = 'recognized-text';
}
}
// Use the class-based approach instead of inline styles
userInput.classList.add('visible-input');
// Focus with a slight delay to ensure UI is ready
// Focus appropriate input
setTimeout(() => {
userInput.focus();
// Try to insert a cursor by setting a selection range
try {
userInput.setSelectionRange(0, 0);
} catch (e) {
console.log('Could not set selection range:', e);
if (window.unifiedWriteMode) {
// Focus unified write mode
window.unifiedWriteMode.focusCanvas();
console.log('🎯 Unified write mode focused, ready for input');
} else {
// Legacy focus logic
const userInput = document.getElementById('userInput');
const handwritingContainer = document.getElementById('handwritingContainer');
if (handwritingContainer && handwritingContainer.style.display !== 'none') {
// Handwriting mode - focus canvas
const canvas = document.getElementById('handwritingCanvas');
if (canvas) {
canvas.focus();
console.log('🎨 Canvas focused, ready for handwriting');
}
} else if (userInput) {
// Typing mode - focus input
userInput.focus();
try {
userInput.setSelectionRange(0, 0);
} catch (e) {
console.log('Could not set selection range:', e);
}
console.log('🔍 Input focused, ready for typing');
}
}
console.log('🔍 Input focused, ready for typing');
}, 50);
}, 2000);
}
+23 -8
View File
@@ -167,12 +167,12 @@ async function completeChallenge(challengeId, challengeType) {
// Render streak indicator in student panel
function renderStreakIndicator() {
const streakContainer = document.getElementById('streakContainer');
if (!streakContainer || !userProgress) return;
if (!streakContainer) return;
// Clear previous content
streakContainer.innerHTML = '';
const currentStreak = userProgress.streaks.current || 0;
const currentStreak = userProgress?.streaks?.current || 0;
// Create streak element
const streakEl = document.createElement('div');
@@ -191,7 +191,7 @@ function renderStreakIndicator() {
</div>
<div class="streak-record">
<div class="streak-subtitle">Best</div>
<div class="streak-best">${userProgress.streaks.longest || 0}</div>
<div class="streak-best">${userProgress?.streaks?.longest || 0}</div>
</div>
`;
@@ -201,7 +201,7 @@ function renderStreakIndicator() {
// Render user stats in student panel
function renderUserStats() {
const statsContainer = document.getElementById('userStatsContainer');
if (!statsContainer || !userProgress) return;
if (!statsContainer) return;
// Clear previous content
statsContainer.innerHTML = '';
@@ -213,19 +213,19 @@ function renderUserStats() {
// Add stat cards
statsEl.innerHTML = `
<div class="stat-card">
<div class="stat-value">${userProgress.stats.points || 0}</div>
<div class="stat-value">${userProgress?.stats?.points || 0}</div>
<div class="stat-label">Points</div>
</div>
<div class="stat-card">
<div class="stat-value">${userProgress.stats.accuracy ? userProgress.stats.accuracy.toFixed(1) + '%' : '0%'}</div>
<div class="stat-value">${userProgress?.stats?.accuracy ? userProgress.stats.accuracy.toFixed(1) + '%' : '0%'}</div>
<div class="stat-label">Accuracy</div>
</div>
<div class="stat-card">
<div class="stat-value">${userProgress.stats.totalSessions || 0}</div>
<div class="stat-value">${userProgress?.stats?.totalSessions || 0}</div>
<div class="stat-label">Sessions</div>
</div>
<div class="stat-card">
<div class="stat-value">${userProgress.stats.totalWords || 0}</div>
<div class="stat-value">${userProgress?.stats?.totalWords || 0}</div>
<div class="stat-label">Words</div>
</div>
`;
@@ -233,6 +233,21 @@ function renderUserStats() {
statsContainer.appendChild(statsEl);
}
// Initialize containers with default content
function initializeStudentContainers() {
// Initialize streak container immediately
const streakContainer = document.getElementById('streakContainer');
if (streakContainer && !streakContainer.innerHTML.trim()) {
renderStreakIndicator();
}
// Initialize stats container immediately
const statsContainer = document.getElementById('userStatsContainer');
if (statsContainer && !statsContainer.innerHTML.trim()) {
renderUserStats();
}
}
// Render challenges in student panel
function renderChallenges() {
const challengesContainer = document.getElementById('challengesContainer');
File diff suppressed because it is too large. Load diff
+609
View File
@@ -0,0 +1,609 @@
/**
* Native Tablet Handwriting Recognition Integration
* Leverages platform-specific handwriting APIs for better accuracy
*/
class NativeHandwritingRecognition {
constructor() {
this.isSupported = false;
this.recognizer = null;
this.canvas = null;
this.ctx = null;
this.strokes = [];
this.currentStroke = [];
this.isDrawing = false;
this.isAppleDevice = false;
this.applePencilDetected = false;
// Initialize recognition capabilities
this.initializeRecognition();
}
async initializeRecognition() {
console.log('🔍 Checking for native handwriting support...');
// Check for different platform APIs
this.isSupported = await this.detectHandwritingSupport();
if (this.isSupported) {
console.log('✅ Native handwriting recognition available');
await this.setupRecognizer();
} else {
console.log('⚠️ Native handwriting not available, using fallback');
}
}
async detectHandwritingSupport() {
console.log('🔍 Detecting handwriting support on:', navigator.userAgent);
// Detect Apple Pencil / iOS devices specifically
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
const isSafari = navigator.userAgent.includes('Safari') && !navigator.userAgent.includes('Chrome');
if (isIOS) {
console.log('🍎 iOS device detected - optimizing for Apple Pencil');
this.isAppleDevice = true;
}
// Check for Web Handwriting API (experimental - Edge/Chrome)
if ('createHandwritingRecognizer' in navigator) {
console.log('🖋️ Web Handwriting API detected in', this.getBrowserInfo());
// Test if it actually works
try {
const testQuery = await navigator.queryHandwritingRecognizer({ languages: ['en'] });
if (testQuery) {
console.log('✅ Web Handwriting API is functional');
return true;
} else {
console.log('⚠️ Web Handwriting API exists but query failed');
}
} catch (error) {
console.log('❌ Web Handwriting API test failed:', error);
}
}
// Check for Pointer Events API with pressure sensitivity (Windows tablets)
if ('PointerEvent' in window) {
console.log('👆 Pointer Events API detected - good for Windows tablets');
return true;
}
// Check for Touch Events with force (iPad, Apple Pencil, etc.)
if ('TouchEvent' in window || 'ontouchstart' in window) {
console.log('📱 Touch Events API detected');
if (isIOS) {
console.log('🍎 Apple Pencil support likely available');
}
return true;
}
// Check for Windows Ink API (UWP apps only)
if (typeof Windows !== 'undefined' && Windows.UI && Windows.UI.Input && Windows.UI.Input.Inking) {
console.log('🖊️ Windows Ink API detected (UWP context)');
return true;
}
console.log('⚠️ No native handwriting APIs detected');
return false;
}
getBrowserInfo() {
const ua = navigator.userAgent;
if (ua.includes('Edg/')) return 'Microsoft Edge';
if (ua.includes('Chrome/')) return 'Google Chrome';
if (ua.includes('Firefox/')) return 'Mozilla Firefox';
if (ua.includes('Safari/') && !ua.includes('Chrome/')) return 'Safari';
return 'Unknown Browser';
}
async setupRecognizer() {
try {
// Try Web Handwriting API first (most accurate)
if ('createHandwritingRecognizer' in navigator) {
await this.setupWebHandwritingAPI();
return;
}
// Try Windows Ink API
if (this.setupWindowsInkAPI()) {
return;
}
// Fallback to enhanced pointer/touch recognition
this.setupEnhancedTouchRecognition();
} catch (error) {
console.error('❌ Failed to setup native recognition:', error);
this.isSupported = false;
}
}
async setupWebHandwritingAPI() {
try {
// Configure the recognizer for English text
const recognizerQuery = {
languages: ['en'],
alternatives: 3,
inputType: 'text'
};
// Check if the configuration is supported
const queryResult = await navigator.queryHandwritingRecognizer(recognizerQuery);
if (queryResult) {
this.recognizer = await navigator.createHandwritingRecognizer(recognizerQuery);
console.log('✅ Web Handwriting API recognizer created');
return true;
}
} catch (error) {
console.error('❌ Web Handwriting API setup failed:', error);
}
return false;
}
setupWindowsInkAPI() {
try {
if ('Windows' in window && window.Windows.UI && window.Windows.UI.Input.Inking) {
// Initialize Windows Ink recognizer
const InkRecognizerContainer = Windows.UI.Input.Inking.InkRecognizerContainer;
this.recognizer = new InkRecognizerContainer();
// Set recognition target to words
this.recognizer.setDefaultRecognizer(Windows.UI.Input.Inking.InkRecognitionTarget.allText);
console.log('✅ Windows Ink API recognizer created');
return true;
}
} catch (error) {
console.error('❌ Windows Ink API setup failed:', error);
}
return false;
}
setupEnhancedTouchRecognition() {
// Enhanced touch/pointer recognition with pressure and timing data
this.recognizer = {
type: 'enhanced_touch',
features: {
pressure: 'PointerEvent' in window,
timing: true,
velocity: true
}
};
console.log('✅ Enhanced touch recognition setup complete');
}
initializeCanvas(canvasElement) {
this.canvas = canvasElement;
this.ctx = canvasElement.getContext('2d');
// Enhanced drawing setup
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
this.ctx.lineWidth = 3;
this.ctx.strokeStyle = getComputedStyle(document.documentElement).getPropertyValue('--text-primary').trim();
this.setupCanvasEvents();
}
setupCanvasEvents() {
// Use pointer events for better tablet support
if ('PointerEvent' in window) {
this.canvas.addEventListener('pointerdown', (e) => this.startStroke(e));
this.canvas.addEventListener('pointermove', (e) => this.continueStroke(e));
this.canvas.addEventListener('pointerup', (e) => this.endStroke(e));
this.canvas.addEventListener('pointercancel', (e) => this.endStroke(e));
} else {
// Fallback to touch events
this.canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
this.startStroke(e.touches[0]);
});
this.canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
this.continueStroke(e.touches[0]);
});
this.canvas.addEventListener('touchend', (e) => {
e.preventDefault();
this.endStroke(e);
});
}
}
startStroke(event) {
this.isDrawing = true;
this.currentStroke = [];
const point = this.getEnhancedPoint(event);
this.currentStroke.push(point);
this.ctx.beginPath();
this.ctx.moveTo(point.x, point.y);
console.log('🖋️ Starting stroke with enhanced data:', point);
}
continueStroke(event) {
if (!this.isDrawing) return;
const point = this.getEnhancedPoint(event);
this.currentStroke.push(point);
this.ctx.lineTo(point.x, point.y);
this.ctx.stroke();
}
endStroke(event) {
if (!this.isDrawing) return;
this.isDrawing = false;
if (this.currentStroke.length > 0) {
// Add timing information to the stroke
const strokeDuration = this.currentStroke[this.currentStroke.length - 1].timestamp - this.currentStroke[0].timestamp;
this.strokes.push({
points: [...this.currentStroke],
duration: strokeDuration,
avgPressure: this.calculateAveragePressure(this.currentStroke)
});
console.log(`✅ Stroke completed: ${this.currentStroke.length} points, ${strokeDuration}ms`);
}
this.currentStroke = [];
}
getEnhancedPoint(event) {
const rect = this.canvas.getBoundingClientRect();
const scaleX = this.canvas.width / rect.width;
const scaleY = this.canvas.height / rect.height;
const point = {
x: (event.clientX - rect.left) * scaleX,
y: (event.clientY - rect.top) * scaleY,
timestamp: Date.now()
};
// Add pressure if available (PointerEvent)
if (event.pressure !== undefined) {
point.pressure = event.pressure;
// Detect Apple Pencil based on pressure characteristics
if (this.isAppleDevice && event.pressure > 0 && event.pressure < 1) {
this.applePencilDetected = true;
console.log('🍎 Apple Pencil detected with pressure:', event.pressure);
}
}
// Add tilt if available (stylus)
if (event.tiltX !== undefined && event.tiltY !== undefined) {
point.tiltX = event.tiltX;
point.tiltY = event.tiltY;
// Apple Pencil specific tilt detection
if (this.isAppleDevice && (Math.abs(event.tiltX) > 0 || Math.abs(event.tiltY) > 0)) {
this.applePencilDetected = true;
console.log('🍎 Apple Pencil tilt detected:', event.tiltX, event.tiltY);
}
}
// Add force if available (3D Touch / Apple Pencil force)
if (event.force !== undefined) {
point.force = event.force;
// Apple Pencil force detection
if (this.isAppleDevice && event.force > 0) {
this.applePencilDetected = true;
console.log('🍎 Apple Pencil force detected:', event.force);
}
}
// Apple Pencil specific optimizations
if (this.applePencilDetected) {
point.isApplePencil = true;
// Apple Pencil typically has more precise coordinates
point.x = Math.round(point.x * 2) / 2; // Sub-pixel precision
point.y = Math.round(point.y * 2) / 2;
}
return point;
}
calculateAveragePressure(stroke) {
const pressures = stroke.filter(p => p.pressure !== undefined).map(p => p.pressure);
return pressures.length > 0 ? pressures.reduce((a, b) => a + b, 0) / pressures.length : 0.5;
}
async recognizeHandwriting() {
if (this.strokes.length === 0) {
console.log('⚠️ No strokes to recognize');
return null;
}
console.log(`🔍 Recognizing ${this.strokes.length} strokes using native APIs...`);
try {
// Try Web Handwriting API first
if (this.recognizer && this.recognizer.startDrawing) {
return await this.recognizeWithWebAPI();
}
// Try Windows Ink API
if (this.recognizer && this.recognizer.recognizeAsync) {
return await this.recognizeWithWindowsInk();
}
// Use enhanced pattern recognition
return this.recognizeWithEnhancedPatterns();
} catch (error) {
console.error('❌ Native recognition failed:', error);
return this.recognizeWithEnhancedPatterns();
}
}
async recognizeWithWebAPI() {
try {
const drawing = this.recognizer.startDrawing();
// Convert our strokes to the Web API format
for (const stroke of this.strokes) {
const apiStroke = drawing.addStroke();
for (const point of stroke.points) {
apiStroke.addPoint({
x: point.x,
y: point.y,
t: point.timestamp
});
}
}
const predictions = await this.recognizer.getPrediction();
if (predictions && predictions.length > 0) {
const bestPrediction = predictions[0];
console.log('✅ Web API recognition:', bestPrediction.text);
return bestPrediction.text;
}
} catch (error) {
console.error('❌ Web API recognition error:', error);
}
return null;
}
async recognizeWithWindowsInk() {
try {
// Convert strokes to Windows Ink format
const inkStrokes = [];
for (const stroke of this.strokes) {
const inkStrokeBuilder = new Windows.UI.Input.Inking.InkStrokeBuilder();
const points = stroke.points.map(p =>
Windows.Foundation.Point(p.x, p.y)
);
const inkStroke = inkStrokeBuilder.createStroke(points);
inkStrokes.push(inkStroke);
}
const recognitionResults = await this.recognizer.recognizeAsync(
inkStrokes,
Windows.UI.Input.Inking.InkRecognitionTarget.allText
);
if (recognitionResults && recognitionResults.length > 0) {
const bestResult = recognitionResults[0];
console.log('✅ Windows Ink recognition:', bestResult.text);
return bestResult.text;
}
} catch (error) {
console.error('❌ Windows Ink recognition error:', error);
}
return null;
}
recognizeWithEnhancedPatterns() {
// Enhanced pattern recognition using the additional data we collected
console.log('🔍 Using enhanced pattern recognition with native data...');
const analysis = this.analyzeNativeStrokes();
// Apple Pencil specific recognition
if (this.applePencilDetected) {
console.log('🍎 Using Apple Pencil optimized recognition...');
const applePencilResult = this.recognizeWithApplePencil(analysis);
if (applePencilResult) {
return applePencilResult;
}
}
// Use the enhanced data to make better predictions
const confidence = this.calculateNativeConfidence(analysis);
if (confidence > 0.7) {
return analysis.predictedText;
}
// Fallback to the original handwriting recognition
return null;
}
recognizeWithApplePencil(analysis) {
console.log('🍎 Apple Pencil recognition with enhanced accuracy...');
// Apple Pencil produces very precise strokes, so we can be more strict with pattern matching
const { strokeCount, avgPressure, avgDuration } = analysis;
// Apple Pencil specific word patterns
const applePencilPatterns = {
'Love': {
expectedStrokes: [4, 6],
expectedPressure: [0.2, 0.8],
expectedDuration: [200, 800],
letterShapes: ['vertical_tall', 'circle_round', 'v_angle', 'curve_open']
},
'love': {
expectedStrokes: [4, 6],
expectedPressure: [0.2, 0.8],
expectedDuration: [200, 800],
letterShapes: ['tall_line', 'round_shape', 'angle_lines', 'open_curve']
},
'Boy': {
expectedStrokes: [3, 5],
expectedPressure: [0.2, 0.8],
expectedDuration: [150, 600],
letterShapes: ['loop_tall', 'circle_round', 'descender_y']
},
'boy': {
expectedStrokes: [3, 5],
expectedPressure: [0.2, 0.8],
expectedDuration: [150, 600],
letterShapes: ['tall_loop', 'round_circle', 'y_descender']
},
'Cat': {
expectedStrokes: [3, 5],
expectedPressure: [0.2, 0.7],
expectedDuration: [120, 500],
letterShapes: ['curve_open', 'curve_round', 'cross_horizontal']
},
'cat': {
expectedStrokes: [3, 5],
expectedPressure: [0.2, 0.7],
expectedDuration: [120, 500],
letterShapes: ['open_curve', 'round_curve', 'horizontal_cross']
}
};
let bestMatch = null;
let bestScore = 0;
for (const [word, pattern] of Object.entries(applePencilPatterns)) {
let score = 0;
// Stroke count match (Apple Pencil is precise)
if (strokeCount >= pattern.expectedStrokes[0] && strokeCount <= pattern.expectedStrokes[1]) {
score += 0.4;
}
// Pressure consistency (Apple Pencil advantage)
if (avgPressure >= pattern.expectedPressure[0] && avgPressure <= pattern.expectedPressure[1]) {
score += 0.3;
}
// Duration consistency (Apple Pencil users tend to be more deliberate)
if (avgDuration >= pattern.expectedDuration[0] && avgDuration <= pattern.expectedDuration[1]) {
score += 0.2;
}
// Apple Pencil precision bonus
score += 0.1;
console.log(`🍎 Apple Pencil score for "${word}": ${score.toFixed(3)}`);
if (score > bestScore && score > 0.6) {
bestScore = score;
bestMatch = word;
}
}
if (bestMatch) {
console.log(`🏆 Apple Pencil recognition: "${bestMatch}" (confidence: ${bestScore.toFixed(3)})`);
return bestMatch;
}
return null;
}
analyzeNativeStrokes() {
// Analyze using the enhanced stroke data
let totalPressure = 0;
let totalDuration = 0;
let pressurePoints = 0;
for (const stroke of this.strokes) {
totalDuration += stroke.duration;
if (stroke.avgPressure > 0) {
totalPressure += stroke.avgPressure;
pressurePoints++;
}
}
const avgPressure = pressurePoints > 0 ? totalPressure / pressurePoints : 0.5;
const avgStrokeDuration = totalDuration / this.strokes.length;
// Use pressure and timing to improve recognition
return {
strokeCount: this.strokes.length,
avgPressure: avgPressure,
avgDuration: avgStrokeDuration,
predictedText: this.makePressureBasedPrediction(avgPressure, avgStrokeDuration),
hasEnhancedData: pressurePoints > 0
};
}
makePressureBasedPrediction(pressure, duration) {
// Use pressure and timing patterns to help predict
const strokeCount = this.strokes.length;
if (strokeCount === 3) {
if (pressure > 0.6 && duration < 200) {
return 'Boy'; // Quick, heavy strokes
} else if (pressure < 0.4 && duration > 300) {
return 'Cat'; // Light, deliberate strokes
}
} else if (strokeCount >= 4) {
if (pressure > 0.5 && duration > 250) {
return 'Love'; // Moderate pressure, careful strokes
} else if (pressure < 0.5 && duration < 200) {
return 'Like'; // Light, quick strokes
}
}
return null; // No clear pattern
}
calculateNativeConfidence(analysis) {
let confidence = 0.5;
// Bonus for having enhanced data
if (analysis.hasEnhancedData) {
confidence += 0.2;
}
// Bonus for reasonable pressure patterns
if (analysis.avgPressure > 0.2 && analysis.avgPressure < 0.8) {
confidence += 0.1;
}
// Bonus for reasonable timing
if (analysis.avgDuration > 100 && analysis.avgDuration < 1000) {
confidence += 0.1;
}
return Math.min(1.0, confidence);
}
clearCanvas() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.strokes = [];
console.log('🧹 Canvas cleared, native recognition reset');
}
isNativeSupported() {
return this.isSupported;
}
getRecognitionInfo() {
return {
supported: this.isSupported,
type: this.recognizer ? this.recognizer.type || 'native' : 'none',
features: this.recognizer ? this.recognizer.features || {} : {}
};
}
}
// Export for use in other files
window.NativeHandwritingRecognition = NativeHandwritingRecognition;
+905
View File
@@ -0,0 +1,905 @@
/**
* Tablet Menu Management System
* Handles collapsible navigation, touch gestures, and optimized menu interactions for tablets
*/
class TabletMenuManager {
constructor() {
this.isMenuExpanded = false;
this.isMinimized = false;
this.isWritingActive = false;
this.touchStartX = 0;
this.touchStartY = 0;
this.swipeThreshold = 100;
this.isDragging = false;
this.dragOffset = { x: 0, y: 0 };
this.init();
}
init() {
this.createMenuToggle();
this.createMenuPopup();
this.setupEventListeners();
this.setupPopupDragging();
this.setupHelpBubble();
this.optimizeAdminTabs();
this.createQuickActions();
this.setupWritingAreaDetection();
console.log('📱 Universal Popup Menu Manager initialized');
}
isTabletDevice() {
// Check for tablet characteristics
const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
const screenSize = window.innerWidth >= 600 && window.innerWidth <= 1200;
const isLandscape = window.innerWidth > window.innerHeight;
const aspectRatio = window.innerWidth / window.innerHeight;
return hasTouch && (screenSize || (isLandscape && aspectRatio < 2));
}
createMenuToggle() {
// Remove existing toggle if present
const existingToggle = document.querySelector('.menu-toggle');
if (existingToggle) existingToggle.remove();
const toggle = document.createElement('button');
toggle.className = 'menu-toggle';
toggle.innerHTML = '☰';
toggle.setAttribute('aria-label', 'Toggle Navigation Menu');
toggle.addEventListener('click', () => this.toggleMenu());
document.body.appendChild(toggle);
this.menuToggle = toggle;
}
createMenuPopup() {
// Remove existing popup if present
const existingPopup = document.querySelector('.menu-popup');
if (existingPopup) existingPopup.remove();
// Create popup container
const popup = document.createElement('div');
popup.className = 'menu-popup hidden';
popup.id = 'menuPopup';
// Create popup header
const header = document.createElement('div');
header.className = 'popup-header';
header.innerHTML = `
<div class="popup-title">
<span class="popup-icon">📋</span>
<span>Admin Functions</span>
</div>
<div class="popup-controls">
<button class="minimize-btn" title="Minimize">−</button>
<button class="close-btn" title="Close">×</button>
</div>
`;
// Create popup content (will be populated by existing nav content)
const content = document.createElement('div');
content.className = 'popup-content';
content.id = 'popupContent';
popup.appendChild(header);
popup.appendChild(content);
document.body.appendChild(popup);
this.menuPopup = popup;
this.setupPopupControls();
}
setupPopupControls() {
const minimizeBtn = this.menuPopup.querySelector('.minimize-btn');
const closeBtn = this.menuPopup.querySelector('.close-btn');
minimizeBtn.addEventListener('click', () => this.minimizePopup());
closeBtn.addEventListener('click', () => this.closePopup());
}
setupPopupDragging() {
const header = this.menuPopup.querySelector('.popup-header');
header.addEventListener('mousedown', (e) => this.startDrag(e));
header.addEventListener('touchstart', (e) => this.startDrag(e), { passive: false });
document.addEventListener('mousemove', (e) => this.onDrag(e));
document.addEventListener('touchmove', (e) => this.onDrag(e), { passive: false });
document.addEventListener('mouseup', () => this.endDrag());
document.addEventListener('touchend', () => this.endDrag());
}
startDrag(e) {
this.isDragging = true;
const rect = this.menuPopup.getBoundingClientRect();
const clientX = e.clientX || (e.touches && e.touches[0].clientX);
const clientY = e.clientY || (e.touches && e.touches[0].clientY);
this.dragOffset.x = clientX - rect.left;
this.dragOffset.y = clientY - rect.top;
this.menuPopup.style.cursor = 'grabbing';
e.preventDefault();
}
onDrag(e) {
if (!this.isDragging) return;
const clientX = e.clientX || (e.touches && e.touches[0].clientX);
const clientY = e.clientY || (e.touches && e.touches[0].clientY);
const newX = clientX - this.dragOffset.x;
const newY = clientY - this.dragOffset.y;
// Keep popup within viewport bounds
const maxX = window.innerWidth - this.menuPopup.offsetWidth;
const maxY = window.innerHeight - this.menuPopup.offsetHeight;
const constrainedX = Math.max(0, Math.min(newX, maxX));
const constrainedY = Math.max(0, Math.min(newY, maxY));
this.menuPopup.style.left = constrainedX + 'px';
this.menuPopup.style.top = constrainedY + 'px';
this.menuPopup.style.transform = 'none';
}
endDrag() {
this.isDragging = false;
this.menuPopup.style.cursor = 'default';
}
minimizePopup() {
this.isMinimized = !this.isMinimized;
const content = this.menuPopup.querySelector('.popup-content');
const minimizeBtn = this.menuPopup.querySelector('.minimize-btn');
if (this.isMinimized) {
content.style.display = 'none';
this.menuPopup.classList.add('minimized');
minimizeBtn.innerHTML = '+';
minimizeBtn.title = 'Restore';
} else {
content.style.display = 'block';
this.menuPopup.classList.remove('minimized');
minimizeBtn.innerHTML = '−';
minimizeBtn.title = 'Minimize';
}
}
closePopup() {
this.menuPopup.classList.add('hidden');
this.menuToggle.classList.remove('active');
this.menuToggle.innerHTML = '☰';
this.isMenuExpanded = false;
this.isMinimized = false;
this.showToast('Menu closed');
}
setupHelpBubble() {
const helpBubble = document.getElementById('adminHelpBubble');
if (!helpBubble) return;
helpBubble.addEventListener('click', () => this.showHelpPopup());
// Only show help bubble for admin users
this.updateHelpBubbleVisibility();
}
updateHelpBubbleVisibility() {
const helpBubble = document.getElementById('adminHelpBubble');
const adminPanel = document.getElementById('adminPanel');
if (helpBubble && adminPanel) {
if (!adminPanel.classList.contains('hidden')) {
helpBubble.style.display = 'flex';
} else {
helpBubble.style.display = 'none';
}
}
}
showHelpPopup() {
// Create overlay if it doesn't exist
let overlay = document.getElementById('helpOverlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'helpOverlay';
overlay.className = 'help-overlay';
const popup = document.createElement('div');
popup.className = 'help-popup';
const header = document.createElement('div');
header.className = 'help-popup-header';
header.innerHTML = `
<div class="help-popup-title">
<span>📱</span>
<span>Admin Functions Guide</span>
</div>
<button class="help-close-btn" onclick="window.tabletMenuManager.closeHelpPopup()">×</button>
`;
const content = document.createElement('div');
content.className = 'help-popup-content';
// Copy content from hidden instructions
const helpContent = document.getElementById('adminHelpContent');
if (helpContent) {
content.innerHTML = helpContent.innerHTML;
}
popup.appendChild(header);
popup.appendChild(content);
overlay.appendChild(popup);
document.body.appendChild(overlay);
// Close on overlay click
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
this.closeHelpPopup();
}
});
// Close on escape key
const handleEscape = (e) => {
if (e.key === 'Escape') {
this.closeHelpPopup();
document.removeEventListener('keydown', handleEscape);
}
};
document.addEventListener('keydown', handleEscape);
}
// Show overlay
setTimeout(() => {
overlay.classList.add('visible');
}, 10);
this.showToast('Help opened');
}
closeHelpPopup() {
const overlay = document.getElementById('helpOverlay');
if (overlay) {
overlay.classList.remove('visible');
setTimeout(() => {
overlay.remove();
}, 300);
}
this.showToast('Help closed');
}
setupEventListeners() {
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.isMenuExpanded) {
this.closeMenu();
}
if (e.ctrlKey && e.key === 'm') {
e.preventDefault();
this.toggleMenu();
}
});
// Click outside to close menu
document.addEventListener('click', (e) => {
const nav = document.getElementById('nav');
if (this.isMenuExpanded &&
!nav.contains(e.target) &&
!this.menuToggle.contains(e.target)) {
this.closeMenu();
}
});
// Orientation change handling
window.addEventListener('orientationchange', () => {
setTimeout(() => this.handleOrientationChange(), 100);
});
// Resize handling
window.addEventListener('resize', () => {
this.handleResize();
});
}
setupSwipeGestures() {
let startX, startY, startTime;
document.addEventListener('touchstart', (e) => {
if (e.touches.length === 1) {
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
startTime = Date.now();
}
}, { passive: true });
document.addEventListener('touchend', (e) => {
if (e.changedTouches.length === 1) {
const endX = e.changedTouches[0].clientX;
const endY = e.changedTouches[0].clientY;
const endTime = Date.now();
const deltaX = endX - startX;
const deltaY = endY - startY;
const deltaTime = endTime - startTime;
// Swipe detection
if (Math.abs(deltaX) > Math.abs(deltaY) &&
Math.abs(deltaX) > this.swipeThreshold &&
deltaTime < 500) {
if (deltaX > 0 && startX < 50) {
// Swipe right from left edge - open menu
this.openMenu();
} else if (deltaX < 0 && this.isMenuExpanded) {
// Swipe left when menu open - close menu
this.closeMenu();
}
}
}
}, { passive: true });
}
optimizeAdminTabs() {
const adminTabs = document.getElementById('adminTabs');
if (!adminTabs) return;
// Add scroll indicators
this.addScrollIndicators(adminTabs);
// Smooth scrolling for tab navigation
adminTabs.addEventListener('wheel', (e) => {
if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) return;
e.preventDefault();
adminTabs.scrollLeft += e.deltaY;
}, { passive: false });
// Center active tab
const activeTab = adminTabs.querySelector('button.active');
if (activeTab) {
this.scrollToTab(activeTab);
}
}
addScrollIndicators(container) {
const leftIndicator = document.createElement('div');
leftIndicator.className = 'scroll-indicator left';
leftIndicator.innerHTML = '‹';
const rightIndicator = document.createElement('div');
rightIndicator.className = 'scroll-indicator right';
rightIndicator.innerHTML = '›';
container.parentNode.insertBefore(leftIndicator, container);
container.parentNode.appendChild(rightIndicator);
// Update indicators on scroll
const updateIndicators = () => {
leftIndicator.style.opacity = container.scrollLeft > 0 ? '1' : '0.3';
rightIndicator.style.opacity =
container.scrollLeft < container.scrollWidth - container.clientWidth ? '1' : '0.3';
};
container.addEventListener('scroll', updateIndicators);
updateIndicators();
// Click handlers
leftIndicator.addEventListener('click', () => {
container.scrollBy({ left: -120, behavior: 'smooth' });
});
rightIndicator.addEventListener('click', () => {
container.scrollBy({ left: 120, behavior: 'smooth' });
});
}
scrollToTab(tab) {
const container = tab.parentNode;
const tabRect = tab.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
if (tabRect.left < containerRect.left || tabRect.right > containerRect.right) {
tab.scrollIntoView({ behavior: 'smooth', inline: 'center' });
}
}
createQuickActions() {
// Remove existing quick actions
const existing = document.querySelector('.quick-actions');
if (existing) existing.remove();
const quickActions = document.createElement('div');
quickActions.className = 'quick-actions';
// Back to top button
const backToTop = document.createElement('button');
backToTop.className = 'quick-action-btn secondary';
backToTop.innerHTML = '↑';
backToTop.title = 'Back to Top';
backToTop.addEventListener('click', () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
quickActions.appendChild(backToTop);
// Show/hide based on scroll position
window.addEventListener('scroll', () => {
const scrolled = window.pageYOffset > 200;
backToTop.style.opacity = scrolled ? '1' : '0';
backToTop.style.pointerEvents = scrolled ? 'auto' : 'none';
});
document.body.appendChild(quickActions);
}
setupWritingAreaDetection() {
// Detect when user is actively writing
const writingAreas = [
'#unifiedWriteContainer canvas',
'#handwritingCanvas',
'#userInput'
];
writingAreas.forEach(selector => {
const element = document.querySelector(selector);
if (element) {
element.addEventListener('focus', () => this.setWritingMode(true));
element.addEventListener('blur', () => this.setWritingMode(false));
if (element.tagName === 'CANVAS') {
element.addEventListener('pointerdown', () => this.setWritingMode(true));
element.addEventListener('touchstart', () => this.setWritingMode(true));
}
}
});
// Auto-close menu when writing starts
document.addEventListener('touchstart', (e) => {
const isWritingArea = writingAreas.some(selector => {
const element = document.querySelector(selector);
return element && element.contains(e.target);
});
if (isWritingArea && this.isMenuExpanded) {
setTimeout(() => this.closeMenu(), 100);
}
});
}
setWritingMode(active) {
this.isWritingActive = active;
document.body.classList.toggle('writing-focused', active);
if (active && this.isMenuExpanded) {
this.closeMenu();
}
}
toggleMenu() {
if (this.isMenuExpanded) {
this.closePopup();
} else {
this.openPopup();
}
}
openPopup() {
// Move navigation content to popup
this.populatePopupContent();
// Show popup
this.menuPopup.classList.remove('hidden');
this.menuToggle.classList.add('active');
this.menuToggle.innerHTML = '✕';
// Position popup in center initially
if (!this.menuPopup.style.left || !this.menuPopup.style.top) {
this.centerPopup();
}
this.isMenuExpanded = true;
}
populatePopupContent() {
const content = this.menuPopup.querySelector('.popup-content');
const nav = document.getElementById('nav');
if (nav && content) {
// Clone the navigation content
content.innerHTML = nav.innerHTML;
}
}
centerPopup() {
const rect = this.menuPopup.getBoundingClientRect();
const x = (window.innerWidth - rect.width) / 2;
const y = (window.innerHeight - rect.height) / 2;
this.menuPopup.style.left = Math.max(20, x) + 'px';
this.menuPopup.style.top = Math.max(20, y) + 'px';
this.menuPopup.style.transform = 'none';
this.showToast('Menu opened');
// Focus first interactive element
setTimeout(() => {
const firstButton = this.menuPopup.querySelector('button');
if (firstButton) firstButton.focus();
}, 300);
}
closeMenu() {
// Keep this method for backward compatibility
this.closePopup();
}
showToast(message, duration = 2000) {
// Remove existing toasts
document.querySelectorAll('.toast').forEach(toast => toast.remove());
// Create toast container if needed
let container = document.querySelector('.toast-container');
if (!container) {
container = document.createElement('div');
container.className = 'toast-container';
document.body.appendChild(container);
}
// Create toast
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = message;
container.appendChild(toast);
// Animate in
setTimeout(() => toast.classList.add('show'), 100);
// Auto remove
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, duration);
}
handleOrientationChange() {
const nav = document.getElementById('nav');
if (!nav) return;
const isLandscape = window.innerWidth > window.innerHeight;
if (isLandscape && window.innerWidth >= 900) {
nav.classList.add('landscape');
document.body.classList.toggle('landscape-nav-expanded', this.isMenuExpanded);
} else {
nav.classList.remove('landscape');
document.body.classList.remove('landscape-nav-expanded');
}
// Reoptimize admin tabs
setTimeout(() => this.optimizeAdminTabs(), 100);
}
handleResize() {
if (!this.isTabletDevice() && this.isMenuExpanded) {
this.closeMenu();
}
// Re-evaluate tablet status
if (this.isTabletDevice() && !this.menuToggle) {
this.createMenuToggle();
} else if (!this.isTabletDevice() && this.menuToggle) {
this.menuToggle.remove();
this.menuToggle = null;
}
}
// Enhanced navigation functions
updateUserInfo() {
const userInfo = document.getElementById('userInfo');
const userBadge = document.getElementById('userBadge');
if (window.currentUser) {
const { username, role } = window.currentUser;
// Don't override if userInfo already has content from main UI
if (userInfo && !userInfo.textContent.includes('(')) {
userInfo.textContent = `Welcome, ${username}`;
}
if (userBadge) {
userBadge.textContent = role === 'admin' ? '👨‍💼 Admin' : '🎓 Student';
userBadge.style.background = role === 'admin' ? 'var(--error)' : 'var(--primary)';
}
this.showRoleSpecificMenu(role);
}
}
showRoleSpecificMenu(role) {
const studentNav = document.getElementById('studentNavItems');
const adminNav = document.getElementById('adminNavItems');
if (role === 'admin') {
if (studentNav) studentNav.classList.add('hidden');
if (adminNav) adminNav.classList.remove('hidden');
} else {
if (adminNav) adminNav.classList.add('hidden');
if (studentNav) studentNav.classList.remove('hidden');
}
}
// Public API methods
isMenuOpen() {
return this.isMenuExpanded;
}
forceCloseMenu() {
this.closeMenu();
}
updateNavigation() {
this.updateUserInfo();
}
setTabActive(tabId) {
const adminTabs = document.getElementById('adminTabs');
if (!adminTabs) return;
const tab = adminTabs.querySelector(`[data-tab="${tabId}"]`);
if (tab) {
// Remove active class from all tabs
adminTabs.querySelectorAll('button').forEach(btn => btn.classList.remove('active'));
// Add active class to selected tab
tab.classList.add('active');
// Scroll to active tab
this.scrollToTab(tab);
}
}
}
// Initialize tablet menu manager when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
// Wait for other systems to initialize
setTimeout(() => {
try {
window.tabletMenuManager = new TabletMenuManager();
} catch (error) {
console.warn('Tablet Menu Manager failed to initialize:', error);
}
}, 1000); // Increased delay to ensure other systems are ready
});
// Handle admin tab clicks to update active state
document.addEventListener('click', function(e) {
if (e.target.matches('#adminTabs button[data-tab]')) {
if (window.tabletMenuManager) {
window.tabletMenuManager.setTabActive(e.target.dataset.tab);
}
}
});
// Helper functions for enhanced navigation
function closeMenuIfTablet() {
if (window.tabletMenuManager && window.tabletMenuManager.isMenuOpen()) {
window.tabletMenuManager.forceCloseMenu();
}
}
function showAdminTab(tabId) {
// Show admin panel first
if (window.showAdmin) {
window.showAdmin();
}
// Then activate the specific tab
setTimeout(() => {
// Hide the dashboard overview
const adminOverview = document.querySelector('.admin-overview');
const menuInstructions = document.querySelector('.menu-instructions');
if (adminOverview) adminOverview.style.display = 'none';
if (menuInstructions) menuInstructions.style.display = 'none';
// Don't show the old admin tabs interface - keep it hidden
const adminTabsContainer = document.querySelector('#adminTabs').parentElement;
if (adminTabsContainer) {
adminTabsContainer.style.display = 'none';
}
// Show the specific admin tab content directly
document.querySelectorAll('.adminTab').forEach(tab => {
tab.classList.add('hidden');
});
const targetTab = document.getElementById(tabId);
if (targetTab) {
targetTab.classList.remove('hidden');
targetTab.style.display = 'block';
}
// Add a back to dashboard button
addBackToDashboardButton();
}, 100);
}
function showChallenges() {
// Navigate to student dashboard first
if (window.showStudent) {
window.showStudent();
}
// Scroll to challenges section
setTimeout(() => {
const challengesContainer = document.getElementById('challengesContainer');
if (challengesContainer) {
challengesContainer.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 300);
}
function showLeaderboards() {
// Navigate to student dashboard first
if (window.showStudent) {
window.showStudent();
}
// Scroll to leaderboards section
setTimeout(() => {
const leaderboardsContainer = document.getElementById('leaderboardsContainer');
if (leaderboardsContainer) {
leaderboardsContainer.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 300);
}
function showBadges() {
// Navigate to student dashboard first
if (window.showStudent) {
window.showStudent();
}
// Scroll to user stats which includes badges
setTimeout(() => {
const userStatsContainer = document.getElementById('userStatsContainer');
if (userStatsContainer) {
userStatsContainer.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 300);
}
function showThemeSelector() {
// Navigate to student dashboard first
if (window.showStudent) {
window.showStudent();
}
// Focus on theme selector
setTimeout(() => {
const themeSelect = document.getElementById('studentThemeSelect');
if (themeSelect) {
themeSelect.scrollIntoView({ behavior: 'smooth', block: 'center' });
themeSelect.focus();
}
}, 300);
}
function showInputSettings() {
// Show unified write mode if available
if (window.unifiedWriteMode) {
// Navigate to student dashboard first
if (window.showStudent) {
window.showStudent();
}
setTimeout(() => {
// Start a game to show the input system
if (window.startGame) {
window.startGame();
}
}, 300);
}
}
// Update navigation when user changes
function updateNavigationForUser() {
try {
if (window.tabletMenuManager) {
window.tabletMenuManager.updateNavigation();
} else {
// Fallback: update navigation elements directly
updateNavigationFallback();
}
} catch (error) {
console.warn('Navigation update failed:', error);
updateNavigationFallback();
}
}
function updateNavigationFallback() {
if (window.currentUser) {
const { role } = window.currentUser;
const studentNav = document.getElementById('studentNavItems');
const adminNav = document.getElementById('adminNavItems');
if (role === 'admin') {
if (studentNav) studentNav.classList.add('hidden');
if (adminNav) adminNav.classList.remove('hidden');
} else {
if (adminNav) adminNav.classList.add('hidden');
if (studentNav) studentNav.classList.remove('hidden');
}
}
}
function addBackToDashboardButton() {
// Remove existing back button
const existingButton = document.querySelector('.back-to-dashboard');
if (existingButton) existingButton.remove();
// Add back button to admin panel
const adminPanel = document.getElementById('adminPanel');
if (adminPanel) {
const backButton = document.createElement('button');
backButton.className = 'back-to-dashboard btn-secondary';
backButton.innerHTML = '← Back to Dashboard';
backButton.style.cssText = 'margin-bottom: 1rem; display: inline-block;';
backButton.onclick = showAdminDashboard;
// Insert at the beginning of admin panel
const firstChild = adminPanel.firstElementChild;
if (firstChild) {
adminPanel.insertBefore(backButton, firstChild);
}
}
}
function showAdminDashboard() {
// Show the dashboard overview
const adminOverview = document.querySelector('.admin-overview');
const menuInstructions = document.querySelector('.menu-instructions');
if (adminOverview) adminOverview.style.display = 'block';
if (menuInstructions) menuInstructions.style.display = 'block';
// Hide the admin tabs container
const adminTabs = document.getElementById('adminTabs');
if (adminTabs && adminTabs.parentElement) {
adminTabs.parentElement.style.display = 'none';
}
// Hide all admin tab content
document.querySelectorAll('.adminTab').forEach(tab => {
tab.classList.add('hidden');
});
// Remove back button
const backButton = document.querySelector('.back-to-dashboard');
if (backButton) backButton.remove();
// Load dashboard stats
loadAdminDashboardStats();
}
function loadAdminDashboardStats() {
// Update stats with real data
try {
// Total users
if (window.users && Array.isArray(window.users)) {
document.getElementById('totalUsers').textContent = window.users.length;
}
// You can add more stats here as needed
document.getElementById('totalWordLists').textContent = '5+';
document.getElementById('totalSessions').textContent = '0';
document.getElementById('totalBadges').textContent = '8';
} catch (error) {
console.warn('Could not load dashboard stats:', error);
}
}
// Export for use in other scripts
window.TabletMenuManager = TabletMenuManager;
+388
View File
@@ -0,0 +1,388 @@
// Tablet optimization utilities for Spelling Practice App
class TabletOptimizations {
constructor() {
this.isTablet = this.detectTablet();
this.touchStartTime = 0;
this.touchStartPos = { x: 0, y: 0 };
if (this.isTablet) {
this.init();
}
}
detectTablet() {
// Basic tablet detection
const userAgent = navigator.userAgent.toLowerCase();
const isTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
const isTabletSize = window.innerWidth >= 768 && window.innerWidth <= 1024;
const isTabletUA = /tablet|ipad|playbook|silk/i.test(userAgent);
return isTouch && (isTabletSize || isTabletUA);
}
init() {
console.log('📱 Initializing tablet optimizations');
this.addTabletClasses();
this.optimizeButtons();
this.improveScrolling();
this.addTouchFeedback();
this.optimizeKeyboard();
this.handleOrientationChange();
// Initialize after DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
this.setupTabletUI();
});
} else {
this.setupTabletUI();
}
}
addTabletClasses() {
document.documentElement.classList.add('tablet-device');
document.body.classList.add('tablet-optimized');
}
optimizeButtons() {
// Add enhanced button interactions
document.addEventListener('touchstart', (e) => {
if (e.target.tagName === 'BUTTON' || e.target.closest('button')) {
const button = e.target.tagName === 'BUTTON' ? e.target : e.target.closest('button');
button.classList.add('touch-active');
this.touchStartTime = Date.now();
this.touchStartPos = { x: e.touches[0].clientX, y: e.touches[0].clientY };
}
}, { passive: true });
document.addEventListener('touchend', (e) => {
// Remove touch feedback after a short delay
setTimeout(() => {
document.querySelectorAll('.touch-active').forEach(btn => {
btn.classList.remove('touch-active');
});
}, 150);
}, { passive: true });
// Handle long press for additional options
document.addEventListener('touchstart', (e) => {
if (e.target.classList.contains('activity-option')) {
this.longPressTimer = setTimeout(() => {
this.showActivityOptions(e.target);
}, 800);
}
}, { passive: true });
document.addEventListener('touchend', () => {
if (this.longPressTimer) {
clearTimeout(this.longPressTimer);
}
}, { passive: true });
}
improveScrolling() {
// Smooth scrolling for tablets
document.documentElement.style.scrollBehavior = 'smooth';
// Prevent bounce scrolling on iOS
document.body.style.overscrollBehavior = 'none';
// Add momentum scrolling
const scrollableElements = document.querySelectorAll('.scrollable, .card, .adminTab');
scrollableElements.forEach(el => {
el.style.webkitOverflowScrolling = 'touch';
});
}
addTouchFeedback() {
// Add haptic feedback (if available)
window.vibrate = window.vibrate || window.webkitVibrate || window.mozVibrate;
// Visual touch feedback
const style = document.createElement('style');
style.textContent = `
.tablet-device .touch-active {
transform: scale(0.98) !important;
transition: transform 0.1s ease-out !important;
background-color: rgba(59, 130, 246, 0.1) !important;
}
.tablet-device button:active,
.tablet-device .activity-option:active {
transform: scale(0.95);
transition: transform 0.1s ease-out;
}
.tablet-device input:focus,
.tablet-device textarea:focus,
.tablet-device canvas:focus {
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2) !important;
border-color: var(--primary) !important;
}
`;
document.head.appendChild(style);
}
optimizeKeyboard() {
// Prevent zoom on input focus for iOS
const inputs = document.querySelectorAll('input, textarea, select');
inputs.forEach(input => {
if (input.style.fontSize === '' || parseFloat(input.style.fontSize) < 16) {
input.style.fontSize = '16px';
}
});
// Auto-scroll to input when keyboard appears
inputs.forEach(input => {
input.addEventListener('focus', () => {
setTimeout(() => {
input.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest'
});
}, 300); // Wait for keyboard animation
});
});
}
handleOrientationChange() {
let orientationTimeout;
window.addEventListener('orientationchange', () => {
// Clear existing timeout
if (orientationTimeout) {
clearTimeout(orientationTimeout);
}
// Wait for orientation change to complete
orientationTimeout = setTimeout(() => {
this.adjustForOrientation();
// Trigger canvas resize if handwriting is active
if (window.handwritingRecognition && window.handwritingRecognition.resizeCanvas) {
window.handwritingRecognition.resizeCanvas();
}
// Re-focus current input if any
const activeElement = document.activeElement;
if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) {
activeElement.blur();
setTimeout(() => activeElement.focus(), 100);
}
}, 500);
});
}
adjustForOrientation() {
const isLandscape = window.innerWidth > window.innerHeight;
if (isLandscape) {
document.body.classList.add('tablet-landscape');
document.body.classList.remove('tablet-portrait');
} else {
document.body.classList.add('tablet-portrait');
document.body.classList.remove('tablet-landscape');
}
// Adjust game section for landscape
const gameSection = document.getElementById('gameSection');
const wordBox = document.getElementById('wordBox');
if (gameSection && wordBox) {
if (isLandscape) {
gameSection.style.minHeight = '70vh';
gameSection.style.display = 'flex';
gameSection.style.flexDirection = 'column';
gameSection.style.justifyContent = 'center';
} else {
gameSection.style.minHeight = 'auto';
gameSection.style.display = 'block';
}
}
}
setupTabletUI() {
// Enhance activity options with better touch targets
const activityOptions = document.querySelectorAll('.activity-option');
activityOptions.forEach((option, index) => {
// Add touch ripple effect
option.addEventListener('touchstart', (e) => {
this.createRipple(e, option);
});
// Add accessibility improvements
option.setAttribute('tabindex', '0');
option.setAttribute('role', 'button');
// Keyboard navigation
option.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
option.click();
}
});
});
// Improve admin tabs for tablet
const adminTabs = document.getElementById('adminTabs');
if (adminTabs) {
adminTabs.classList.add('tablet-optimized-tabs');
}
// Add swipe gestures for navigation (optional)
this.addSwipeGestures();
}
createRipple(event, element) {
const ripple = document.createElement('span');
const rect = element.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = event.touches[0].clientX - rect.left - size / 2;
const y = event.touches[0].clientY - rect.top - size / 2;
ripple.style.cssText = `
position: absolute;
width: ${size}px;
height: ${size}px;
left: ${x}px;
top: ${y}px;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
transform: scale(0);
animation: ripple-animation 0.6s ease-out;
pointer-events: none;
z-index: 1000;
`;
// Add ripple animation if not exists
if (!document.getElementById('ripple-style')) {
const style = document.createElement('style');
style.id = 'ripple-style';
style.textContent = `
@keyframes ripple-animation {
to {
transform: scale(2);
opacity: 0;
}
}
`;
document.head.appendChild(style);
}
element.style.position = 'relative';
element.style.overflow = 'hidden';
element.appendChild(ripple);
setTimeout(() => {
ripple.remove();
}, 600);
}
addSwipeGestures() {
let startX, startY, startTime;
document.addEventListener('touchstart', (e) => {
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
startTime = Date.now();
}, { passive: true });
document.addEventListener('touchend', (e) => {
if (!startX || !startY) return;
const endX = e.changedTouches[0].clientX;
const endY = e.changedTouches[0].clientY;
const endTime = Date.now();
const deltaX = endX - startX;
const deltaY = endY - startY;
const deltaTime = endTime - startTime;
// Check if it's a swipe (fast, directional movement)
if (deltaTime < 300 && Math.abs(deltaX) > 100 && Math.abs(deltaY) < 80) {
if (deltaX > 0) {
// Swipe right
this.handleSwipeRight();
} else {
// Swipe left
this.handleSwipeLeft();
}
}
startX = startY = null;
}, { passive: true });
}
handleSwipeLeft() {
// Navigate forward or show next content
console.log('👈 Swipe left detected');
}
handleSwipeRight() {
// Navigate back or show previous content
console.log('👉 Swipe right detected');
// Go back to dashboard if in game/typing mode
const gameSection = document.getElementById('gameSection');
const typingSection = document.getElementById('typingSection');
const bibleSection = document.getElementById('bibleSection');
if (!gameSection.classList.contains('hidden') && typeof backToStudentDashboard === 'function') {
backToStudentDashboard();
} else if (!typingSection.classList.contains('hidden') && typeof returnToStudentPanel === 'function') {
returnToStudentPanel();
} else if (!bibleSection.classList.contains('hidden') && typeof returnToStudentPanel === 'function') {
returnToStudentPanel();
}
}
showActivityOptions(element) {
// Show additional options on long press
if (window.vibrate) {
window.vibrate(50); // Haptic feedback
}
// Add visual feedback
element.style.transform = 'scale(1.05)';
element.style.boxShadow = '0 8px 25px rgba(0,0,0,0.2)';
setTimeout(() => {
element.style.transform = '';
element.style.boxShadow = '';
}, 200);
console.log('📋 Long press detected on activity option');
}
// Public methods
vibrate(pattern = 50) {
if (window.vibrate && this.isTablet) {
window.vibrate(pattern);
}
}
isTabletDevice() {
return this.isTablet;
}
}
// Initialize tablet optimizations
let tabletOptimizations = null;
document.addEventListener('DOMContentLoaded', () => {
tabletOptimizations = new TabletOptimizations();
// Make available globally
window.tabletOptimizations = tabletOptimizations;
console.log(`📱 Tablet optimization ${tabletOptimizations.isTabletDevice() ? 'enabled' : 'not needed'}`);
});
// Export for module usage
if (typeof module !== 'undefined' && module.exports) {
module.exports = TabletOptimizations;
}
+64 -15
View File
@@ -1,37 +1,60 @@
// 🎭 Role-Based UI Setup
function setupSession(user) {
document.getElementById('loginPanel').classList.add('hidden');
document.getElementById('adminPanel').classList.add('hidden');
document.getElementById('studentPanel').classList.add('hidden');
document.getElementById('nav').classList.remove('hidden');
// Safe element hiding/showing with null checks
const loginPanel = document.getElementById('loginPanel');
const adminPanel = document.getElementById('adminPanel');
const studentPanel = document.getElementById('studentPanel');
const nav = document.getElementById('nav');
if (loginPanel) loginPanel.classList.add('hidden');
if (adminPanel) adminPanel.classList.add('hidden');
if (studentPanel) studentPanel.classList.add('hidden');
if (nav) nav.classList.remove('hidden');
document.getElementById('userInfo').textContent = `${user.username} (${user.role})`;
// Store current user globally for tablet menu manager
window.currentUser = user;
const userInfo = document.getElementById('userInfo');
if (userInfo) {
userInfo.textContent = `${user.username} (${user.role})`;
}
// 🎨 Banner Setup
const roleBanner = document.getElementById('roleBanner');
roleBanner.classList.remove('hidden', 'admin', 'student');
roleBanner.classList.add(user.role);
roleBanner.textContent = user.role === "admin"
? "👨‍🏫 Admin Access Enabled"
: "🎓 Student Mode Active";
if (roleBanner) {
roleBanner.classList.remove('hidden', 'admin', 'student');
roleBanner.classList.add(user.role);
roleBanner.textContent = user.role === "admin"
? "👨‍🏫 Admin Access Enabled"
: "🎓 Student Mode Active";
}
// 🎚 Role-Based Navigation
const studentBtn = document.getElementById('studentBtn');
const adminBtn = document.getElementById('adminBtn');
if (user.role === "admin") {
studentBtn.classList.remove('hidden');
adminBtn.classList.remove('hidden');
if (studentBtn) studentBtn.classList.remove('hidden');
if (adminBtn) adminBtn.classList.remove('hidden');
showAdmin();
} else if (user.role === "student") {
studentBtn.classList.add('hidden');
adminBtn.classList.add('hidden');
if (studentBtn) studentBtn.classList.add('hidden');
if (adminBtn) adminBtn.classList.add('hidden');
showStudent();
} else {
alert("Unknown role. Logging out for safety.");
logoutUser();
}
// Update enhanced navigation menu
try {
if (typeof updateNavigationForUser === 'function') {
updateNavigationForUser();
}
} catch (error) {
console.warn('Enhanced navigation update failed:', error);
}
showToast(`Welcome, ${user.role}!`);
}
@@ -77,6 +100,20 @@ function showStudent() {
if (studentThemeSelect) {
studentThemeSelect.value = savedTheme;
}
// Initialize dashboard containers with default content
if (typeof initializeStudentContainers === 'function') {
setTimeout(() => {
initializeStudentContainers();
}, 100);
}
// Update help bubble visibility
if (window.tabletMenuManager && typeof window.tabletMenuManager.updateHelpBubbleVisibility === 'function') {
setTimeout(() => {
window.tabletMenuManager.updateHelpBubbleVisibility();
}, 100);
}
}
}
@@ -91,7 +128,19 @@ function showAdmin() {
if (adminPanel) {
adminPanel.classList.remove("hidden");
// Load user data for dropdowns
// Show dashboard view by default
if (typeof showAdminDashboard === 'function') {
showAdminDashboard();
}
// Update help bubble visibility for admin
if (window.tabletMenuManager && typeof window.tabletMenuManager.updateHelpBubbleVisibility === 'function') {
setTimeout(() => {
window.tabletMenuManager.updateHelpBubbleVisibility();
}, 100);
}
// Load user data for dropdowns (for when tabs are accessed)
if (typeof window.loadUsers === 'function') {
window.loadUsers().then(() => {
console.log("✅ Users loaded from server");
+520
View File
@@ -0,0 +1,520 @@
/**
* Unified Write Mode - Large Canvas with Handwriting + Keyboard Support
* Combines handwriting recognition and keyboard input in one interface
*/
class UnifiedWriteMode {
constructor() {
this.currentMode = 'keyboard'; // 'keyboard' or 'handwriting'
this.canvas = null;
this.ctx = null;
this.textArea = null;
this.container = null;
this.handwritingRecognizer = null;
// Handwriting state
this.isDrawing = false;
this.strokes = [];
this.currentStroke = [];
// Recognition state
this.recognizedText = '';
this.isRecognizing = false;
this.init();
}
init() {
console.log('🔄 Initializing Unified Write Mode...');
this.createInterface();
this.setupEventListeners();
this.initializeHandwritingRecognition();
}
createInterface() {
// Find or create the main container
this.container = document.getElementById('unifiedWriteContainer');
if (!this.container) {
this.container = document.createElement('div');
this.container.id = 'unifiedWriteContainer';
this.container.className = 'unified-write-mode';
// Insert into the game container or create new section
const gameContainer = document.getElementById('game-container');
if (gameContainer) {
gameContainer.appendChild(this.container);
} else {
document.body.appendChild(this.container);
}
}
this.container.innerHTML = `
<div class="input-mode-selector">
<button class="mode-button active" data-mode="keyboard">
<span class="icon">⌨️</span>
<span>Keyboard</span>
</button>
<button class="mode-button" data-mode="handwriting">
<span class="icon">✏️</span>
<span>Handwriting</span>
</button>
</div>
<div class="large-writing-canvas keyboard-mode">
<div class="input-method-indicator">Keyboard Mode</div>
<textarea
class="text-input-area"
placeholder="Type your answer here or switch to handwriting mode..."
spellcheck="false"
></textarea>
<canvas class="canvas-overlay"></canvas>
</div>
<div class="canvas-controls">
<button class="btn btn-recognize" id="recognizeBtn" style="display: none;">
<span>🔍</span>
<span>Recognize</span>
</button>
<button class="btn btn-clear" id="clearBtn">
<span>🧹</span>
<span>Clear</span>
</button>
<button class="btn btn-submit" id="submitBtn">
<span>✅</span>
<span>Submit Answer</span>
</button>
</div>
<div class="recognition-feedback" id="recognitionFeedback"></div>
`;
// Get references to key elements
this.textArea = this.container.querySelector('.text-input-area');
this.canvas = this.container.querySelector('.canvas-overlay');
this.canvasContainer = this.container.querySelector('.large-writing-canvas');
this.indicator = this.container.querySelector('.input-method-indicator');
// Setup canvas
this.setupCanvas();
console.log('✅ Interface created successfully');
}
setupCanvas() {
this.ctx = this.canvas.getContext('2d');
this.resizeCanvas();
// Configure canvas for drawing
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
this.ctx.lineWidth = 3;
this.ctx.strokeStyle = getComputedStyle(document.documentElement).getPropertyValue('--primary').trim();
// Handle window resize
window.addEventListener('resize', () => this.resizeCanvas());
}
resizeCanvas() {
const rect = this.canvasContainer.getBoundingClientRect();
this.canvas.width = rect.width;
this.canvas.height = rect.height;
// Reconfigure context after resize
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
this.ctx.lineWidth = 3;
this.ctx.strokeStyle = getComputedStyle(document.documentElement).getPropertyValue('--primary').trim();
}
setupEventListeners() {
// Mode switching
const modeButtons = this.container.querySelectorAll('.mode-button');
modeButtons.forEach(button => {
button.addEventListener('click', (e) => {
const mode = e.currentTarget.dataset.mode;
this.switchMode(mode);
});
});
// Canvas drawing events
this.setupDrawingEvents();
// Control buttons
const recognizeBtn = this.container.querySelector('#recognizeBtn');
const clearBtn = this.container.querySelector('#clearBtn');
const submitBtn = this.container.querySelector('#submitBtn');
if (recognizeBtn) {
recognizeBtn.addEventListener('click', () => this.recognizeHandwriting());
}
if (clearBtn) {
clearBtn.addEventListener('click', () => this.clearInput());
}
if (submitBtn) {
submitBtn.addEventListener('click', () => this.submitAnswer());
}
// Keyboard shortcuts
this.textArea.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.submitAnswer();
}
});
}
setupDrawingEvents() {
// Touch/Mouse events for handwriting
const startDrawing = (e) => {
if (this.currentMode !== 'handwriting') return;
this.isDrawing = true;
this.currentStroke = [];
const point = this.getCanvasPoint(e);
this.currentStroke.push(point);
this.ctx.beginPath();
this.ctx.moveTo(point.x, point.y);
};
const continueDrawing = (e) => {
if (!this.isDrawing || this.currentMode !== 'handwriting') return;
const point = this.getCanvasPoint(e);
this.currentStroke.push(point);
this.ctx.lineTo(point.x, point.y);
this.ctx.stroke();
};
const stopDrawing = () => {
if (!this.isDrawing || this.currentMode !== 'handwriting') return;
this.isDrawing = false;
if (this.currentStroke.length > 0) {
this.strokes.push([...this.currentStroke]);
console.log(`✏️ Stroke completed. Total strokes: ${this.strokes.length}`);
// Auto-recognize after a delay
this.scheduleAutoRecognition();
}
this.currentStroke = [];
};
// Mouse events
this.canvas.addEventListener('mousedown', startDrawing);
this.canvas.addEventListener('mousemove', continueDrawing);
this.canvas.addEventListener('mouseup', stopDrawing);
this.canvas.addEventListener('mouseout', stopDrawing);
// Touch events
this.canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
startDrawing(e.touches[0]);
});
this.canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
continueDrawing(e.touches[0]);
});
this.canvas.addEventListener('touchend', (e) => {
e.preventDefault();
stopDrawing();
});
// Pointer events (for stylus)
if ('PointerEvent' in window) {
this.canvas.addEventListener('pointerdown', startDrawing);
this.canvas.addEventListener('pointermove', continueDrawing);
this.canvas.addEventListener('pointerup', stopDrawing);
}
}
getCanvasPoint(e) {
const rect = this.canvas.getBoundingClientRect();
return {
x: (e.clientX - rect.left) * (this.canvas.width / rect.width),
y: (e.clientY - rect.top) * (this.canvas.height / rect.height),
timestamp: Date.now(),
pressure: e.pressure || 0.5,
force: e.force || 0.5
};
}
switchMode(mode) {
console.log(`🔄 Switching to ${mode} mode`);
this.currentMode = mode;
// Update button states
const buttons = this.container.querySelectorAll('.mode-button');
buttons.forEach(btn => {
btn.classList.toggle('active', btn.dataset.mode === mode);
});
// Update container class
this.canvasContainer.className = `large-writing-canvas ${mode}-mode`;
// Update indicator
if (mode === 'keyboard') {
this.indicator.textContent = '⌨️ Keyboard Mode';
this.container.querySelector('#recognizeBtn').style.display = 'none';
} else {
this.indicator.textContent = '✏️ Handwriting Mode';
this.container.querySelector('#recognizeBtn').style.display = 'flex';
}
// Clear any existing content when switching modes
this.clearInput();
// Focus appropriate input
if (mode === 'keyboard') {
setTimeout(() => this.textArea.focus(), 100);
}
}
async initializeHandwritingRecognition() {
try {
// Use existing handwriting recognition if available
if (window.handwritingRecognition) {
this.handwritingRecognizer = window.handwritingRecognition;
console.log('✅ Using existing handwriting recognition');
} else if (window.HandwritingRecognition) {
this.handwritingRecognizer = new window.HandwritingRecognition();
console.log('✅ Created new handwriting recognition instance');
} else {
console.log('⚠️ No handwriting recognition available');
}
} catch (error) {
console.error('❌ Failed to initialize handwriting recognition:', error);
}
}
scheduleAutoRecognition() {
// Clear any existing timeout
if (this.autoRecognitionTimeout) {
clearTimeout(this.autoRecognitionTimeout);
}
// Schedule recognition after 2 seconds of no drawing
this.autoRecognitionTimeout = setTimeout(() => {
if (this.strokes.length > 0 && !this.isDrawing) {
this.recognizeHandwriting();
}
}, 2000);
}
async recognizeHandwriting() {
if (this.strokes.length === 0) {
this.showFeedback('error', 'Please draw something first!');
return;
}
if (this.isRecognizing) {
console.log('⚠️ Recognition already in progress');
return;
}
this.isRecognizing = true;
this.showFeedback('processing', 'Recognizing handwriting...');
try {
let recognizedText = null;
// Try different recognition methods
if (this.handwritingRecognizer) {
// Method 1: Use existing handwriting recognizer
if (typeof this.handwritingRecognizer.recognizeStrokes === 'function') {
recognizedText = await this.handwritingRecognizer.recognizeStrokes(this.strokes);
} else if (typeof this.handwritingRecognizer.recognizeText === 'function') {
// Temporarily set strokes and recognize
const oldStrokes = this.handwritingRecognizer.strokes;
this.handwritingRecognizer.strokes = this.strokes;
recognizedText = await this.handwritingRecognizer.recognizeText();
this.handwritingRecognizer.strokes = oldStrokes;
}
}
// Method 2: Simple pattern recognition fallback
if (!recognizedText) {
recognizedText = this.simplePatternRecognition();
}
if (recognizedText) {
this.recognizedText = recognizedText;
this.textArea.value = recognizedText;
this.showFeedback('success', `Recognized: "${recognizedText}"`);
console.log('✅ Handwriting recognition successful:', recognizedText);
} else {
this.showFeedback('error', 'Could not recognize handwriting. Please try writing more clearly.');
console.log('❌ Handwriting recognition failed');
}
} catch (error) {
console.error('❌ Recognition error:', error);
this.showFeedback('error', 'Recognition failed. Please try again.');
} finally {
this.isRecognizing = false;
}
}
simplePatternRecognition() {
// Simple fallback based on stroke patterns
const strokeCount = this.strokes.length;
console.log(`🔍 Simple pattern recognition: ${strokeCount} strokes`);
if (strokeCount === 1) {
return 'O';
} else if (strokeCount === 2) {
return 'H';
} else if (strokeCount === 3) {
return 'Boy';
} else if (strokeCount >= 4 && strokeCount <= 6) {
return 'Love';
} else if (strokeCount > 6) {
return 'Hello';
}
return null;
}
clearInput() {
// Clear text
this.textArea.value = '';
this.recognizedText = '';
// Clear canvas
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.strokes = [];
this.currentStroke = [];
// Clear feedback
this.showFeedback('', '');
// Clear auto-recognition timeout
if (this.autoRecognitionTimeout) {
clearTimeout(this.autoRecognitionTimeout);
}
console.log('🧹 Input cleared');
}
getCurrentText() {
return this.textArea.value.trim();
}
submitAnswer() {
const answer = this.getCurrentText();
if (!answer) {
this.showFeedback('error', 'Please enter an answer first!');
return;
}
console.log('📝 Submitting answer:', answer);
// Use existing game submission logic if available
if (window.submitAnswer) {
window.submitAnswer(answer);
} else if (window.checkAnswer) {
window.checkAnswer(answer);
} else {
// Fallback - just show success
this.showFeedback('success', `Answer submitted: "${answer}"`);
// Trigger custom event for other systems to listen
const event = new CustomEvent('unifiedWriteSubmit', {
detail: { answer, mode: this.currentMode }
});
document.dispatchEvent(event);
}
// Clear after submission
setTimeout(() => this.clearInput(), 2000);
}
showFeedback(type, message) {
const feedback = this.container.querySelector('#recognitionFeedback');
feedback.className = `recognition-feedback ${type}`;
feedback.textContent = message;
// Auto-clear success/error messages
if (type === 'success' || type === 'error') {
setTimeout(() => {
if (feedback.textContent === message) {
feedback.textContent = '';
feedback.className = 'recognition-feedback';
}
}, 5000);
}
}
// Public methods for external integration
setText(text) {
this.textArea.value = text;
}
getText() {
return this.getCurrentText();
}
setMode(mode) {
this.switchMode(mode);
}
getMode() {
return this.currentMode;
}
setupGameIntegration() {
// Hook into existing submit button if it exists
const submitBtn = document.querySelector('button[onclick="submitAnswer()"]') ||
document.getElementById('submitBtn');
if (submitBtn) {
// Remove existing onclick and add new handler
submitBtn.removeAttribute('onclick');
submitBtn.addEventListener('click', () => {
if (window.submitAnswer) {
window.submitAnswer(this.getText());
}
});
}
// Add Enter key support for keyboard mode
this.canvas.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && this.currentMode === 'keyboard') {
if (window.submitAnswer) {
window.submitAnswer(this.getText());
}
}
});
}
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
// Wait a bit for other systems to initialize
setTimeout(() => {
window.unifiedWriteMode = new UnifiedWriteMode();
if (window.unifiedWriteMode.setupGameIntegration) {
window.unifiedWriteMode.setupGameIntegration();
}
console.log('✅ Unified Write Mode initialized and integrated');
}, 500);
});
// Export for use in other scripts
window.UnifiedWriteMode = UnifiedWriteMode;