diff --git a/app.py b/app.py index 7f9a077..1a49e15 100644 --- a/app.py +++ b/app.py @@ -140,6 +140,36 @@ with app.app_context(): db.create_all() print("Datenbanktabellen wurden initialisiert") + # Prüfe ob die lesson_statistic Tabelle die alte Spalte chars_per_second hat + from sqlalchemy import inspect, text + inspector = inspect(db.engine) + if 'lesson_statistic' in inspector.get_table_names(): + columns = [col['name'] for col in inspector.get_columns('lesson_statistic')] + if 'chars_per_second' in columns and 'chars_per_minute' not in columns: + print("Aktualisiere Datenbanktabelle lesson_statistic...") + # Temporäre Tabelle erstellen + db.session.execute(text(''' + CREATE TABLE lesson_statistic_new ( + id INTEGER PRIMARY KEY, + lesson_index INTEGER NOT NULL, + chars_per_minute FLOAT DEFAULT 0.0, + error_rate FLOAT DEFAULT 0.0, + wpm FLOAT DEFAULT 0.0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + ''')) + # Daten umrechnen und übertragen (chars_per_second * 60) + db.session.execute(text(''' + INSERT INTO lesson_statistic_new (id, lesson_index, chars_per_minute, error_rate, wpm, created_at) + SELECT id, lesson_index, chars_per_second * 60, error_rate, wpm, created_at + FROM lesson_statistic + ''')) + # Alte Tabelle löschen und neue umbenennen + db.session.execute(text('DROP TABLE lesson_statistic')) + db.session.execute(text('ALTER TABLE lesson_statistic_new RENAME TO lesson_statistic')) + db.session.commit() + print("Datenbanktabelle lesson_statistic erfolgreich aktualisiert") + # Versuche Lektionen zu laden load_lessons() @@ -363,14 +393,14 @@ def calculate_statistics(user_input, current_text, elapsed_time): # Berechne Metriken basierend auf echter Zeit if total_chars > 0 and elapsed_time > 0: - # Zeichen pro Sekunde (auf eine Nachkommastelle gerundet) - chars_per_second = total_chars / elapsed_time + # Zeichen pro Minute (ganzzahlig) + chars_per_minute = (total_chars / elapsed_time) * 60 # Wörter pro Minute gemäß Formel: (Gesamtzahl der korrekt getippten Zeichen/5)/((Zeit in Sekunden)/60) words_per_minute = (correct_chars / 5) / (elapsed_time / 60) # Durchschnittliche Zeit pro Zeichen duration_per_char = elapsed_time / total_chars else: - chars_per_second = 0 + chars_per_minute = 0 words_per_minute = 0 duration_per_char = 0 @@ -383,7 +413,7 @@ def calculate_statistics(user_input, current_text, elapsed_time): 'incorrect_chars': incorrect_chars, 'total_chars': total_chars, 'error_rate': round(error_rate, 1), - 'typing_speed': round(chars_per_second, 1), # Auf eine Nachkommastelle + 'typing_speed': round(chars_per_minute), # Ganzzahlig 'duration_per_char': round(duration_per_char, 3), 'words_per_minute': round(words_per_minute, 2), # Auf zwei Nachkommastellen 'accuracy': round(accuracy, 2) @@ -470,7 +500,7 @@ def save_lesson_statistics(): try: data = request.get_json() lesson_index = data.get('lesson_index') - chars_per_second = data.get('chars_per_second') + chars_per_minute = data.get('chars_per_minute') error_rate = data.get('error_rate') wpm = data.get('wpm') @@ -480,7 +510,7 @@ def save_lesson_statistics(): # Neue Statistik für die Lektion erstellen lesson_stat = LessonStatistic( lesson_index=lesson_index, - chars_per_second=chars_per_second, + chars_per_minute=chars_per_minute, error_rate=error_rate, wpm=wpm ) @@ -494,6 +524,51 @@ def save_lesson_statistics(): print(f"Fehler in save_lesson_statistics: {e}") return jsonify({'error': 'Interner Serverfehler'}), 500 +@app.route('/statistics') +def statistics(): + """Zeigt die Statistik-Seite mit Diagrammen""" + try: + # Hole den aktuellen Fortschritt für die voreingestellte Lektion + progress = Progress.query.first() + current_lesson_index = progress.current_text_index if progress else 0 + + # Prüfe für jede Lektion, ob Statistik-Daten vorhanden sind + lesson_has_data = [] + for i in range(len(lessons)): + has_data = LessonStatistic.query.filter_by(lesson_index=i).first() is not None + lesson_has_data.append(has_data) + + return render_template('statistics.html', + lessons=lessons, + current_lesson_index=current_lesson_index, + lesson_has_data=lesson_has_data) + except Exception as e: + print(f"Fehler in statistics(): {e}") + return render_template('statistics.html', + lessons=[], + current_lesson_index=0, + lesson_has_data=[]) + +@app.route('/get_lesson_statistics/') +def get_lesson_statistics(lesson_index): + """Gibt die Statistiken für eine bestimmte Lektion zurück""" + try: + stats = LessonStatistic.query.filter_by(lesson_index=lesson_index)\ + .order_by(LessonStatistic.created_at.asc())\ + .all() + + data = { + 'dates': [stat.created_at.strftime('%y-%m-%d') for stat in stats], + 'chars_per_minute': [stat.chars_per_minute for stat in stats], + 'error_rate': [stat.error_rate for stat in stats], + 'wpm': [stat.wpm for stat in stats] + } + + return jsonify(data) + except Exception as e: + print(f"Fehler in get_lesson_statistics: {e}") + return jsonify({'error': 'Interner Serverfehler'}), 500 + @app.route('/end_session', methods=['POST']) def end_session(): """Beendet eine Tipp-Session und speichert Statistiken""" diff --git a/data/lessons.json b/data/lessons.json index 506af16..4ab3dd9 100644 --- a/data/lessons.json +++ b/data/lessons.json @@ -42,7 +42,7 @@ { "lesson": 9, "title": "Kurze Textpassagen", - "text": "Das schnelle Tippen mit zehn Fingern spart viel Zeit. Mit Geduld und Übung wird jeder Tippfehler seltener. Eine gute Haltung und ruhige Atmung helfen beim Lernen.\nBleibe konzentriert, atme ruhig und halte den Blick auf den Text." + "text": "Das schnelle Tippen mit zehn Fingern spart viel Zeit. Mit Geduld und Übung wird jeder Tippfehler seltener. Eine gute Haltung und ruhige Atmung helfen beim Lernen.\nBleibe konzentriert, atme ruhig und halte den Blick auf dem Text." }, { "lesson": 10, diff --git a/models.py b/models.py index aaf6624..fb13bb9 100644 --- a/models.py +++ b/models.py @@ -22,7 +22,7 @@ class Statistic(db.Model): class LessonStatistic(db.Model): id = db.Column(db.Integer, primary_key=True) lesson_index = db.Column(db.Integer, nullable=False) - chars_per_second = db.Column(db.Float, default=0.0) + chars_per_minute = db.Column(db.Float, default=0.0) error_rate = db.Column(db.Float, default=0.0) wpm = db.Column(db.Float, default=0.0) created_at = db.Column(db.DateTime, default=db.func.now()) diff --git a/static/css/style.css b/static/css/style.css index 15b998c..7f655a0 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -101,6 +101,19 @@ body { color: #fff; } +/* Statistik Link Styling */ +.statistics-link { + cursor: pointer; + transition: all 0.3s ease; + color: inherit; +} + +.statistics-link:hover { + color: #ffc107 !important; + text-shadow: 0 0 10px #ffc107, 0 0 20px #ffc107; + transform: scale(1.05); +} + .card { border: none; border-radius: 10px; diff --git a/static/js/script.js b/static/js/script.js index 0c84571..cad2116 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -62,9 +62,27 @@ document.addEventListener('DOMContentLoaded', function() { // Starte die Zeitmessung beim ersten Tastendruck und zähle Tastaturanschläge userInput.addEventListener('keydown', function(event) { + const cursorPosition = userInput.selectionStart; + const textLength = userInput.value.length; + let shouldCountKey = false; + // Zähle Tastaturanschläge für alle Tasten, die keine Steuertasten sind (inkl. Backspace) // Beachte: event.key für Backspace ist 'Backspace' if (event.key.length === 1 || event.key === 'Backspace') { + shouldCountKey = true; + } + // Pfeiltasten: Nur zählen, wenn sich der Cursor tatsächlich bewegt + else if (event.key === 'ArrowLeft') { + if (cursorPosition > 0) { + shouldCountKey = true; + } + } else if (event.key === 'ArrowRight') { + if (cursorPosition < textLength) { + shouldCountKey = true; + } + } + + if (shouldCountKey) { if (!startTime) { startTime = new Date(); } @@ -95,9 +113,9 @@ document.addEventListener('DOMContentLoaded', function() { // Speichere die Statistiken für diese Lektion const lessonIndex = window.currentLessonIndex; - const charsPerSecond = parseFloat(speedElement.textContent); + const charsPerMinute = parseInt(speedElement.textContent); const errorRate = parseFloat(errorRateElement.textContent); - const wpm = parseFloat(wpmElement.textContent); + const wpm = parseInt(wpmElement.textContent); fetch('/save_lesson_statistics', { method: 'POST', @@ -106,7 +124,7 @@ document.addEventListener('DOMContentLoaded', function() { }, body: JSON.stringify({ lesson_index: lessonIndex, - chars_per_second: charsPerSecond, + chars_per_minute: charsPerMinute, error_rate: errorRate, wpm: wpm }) @@ -272,12 +290,12 @@ document.addEventListener('DOMContentLoaded', function() { } let incorrectKeyStrokes = totalKeyStrokes - correctKeyStrokes; - let charsPerSecond = elapsedTime > 0 ? correctKeyStrokes / elapsedTime : 0; + let charsPerMinute = elapsedTime > 0 ? (correctKeyStrokes / elapsedTime) * 60 : 0; let errorRate = correctKeyStrokes > 0 ? (incorrectKeyStrokes * 100.0) / correctKeyStrokes : 0; let wpm = elapsedTime > 0 ? (correctKeyStrokes / 5) / (elapsedTime / 60) : 0; // Aktualisiere die Anzeige - speedElement.textContent = charsPerSecond.toFixed(1); + speedElement.textContent = Math.round(charsPerMinute); errorRateElement.textContent = errorRate.toFixed(1); correctElement.textContent = correctKeyStrokes; incorrectElement.textContent = incorrectKeyStrokes; @@ -346,6 +364,11 @@ document.addEventListener('DOMContentLoaded', function() { updateTextHighlighting('', lines[currentLineIndex]); } + // Event-Listener für Statistik-Link + document.querySelector('.statistics-link').addEventListener('click', function() { + window.location.href = '/statistics'; + }); + // Initialisiere Statistik und Textfarben updateLineDisplay(); updateStatistics('', lines[currentLineIndex]); diff --git a/templates/index.html b/templates/index.html index 073e0a5..e1208a6 100644 --- a/templates/index.html +++ b/templates/index.html @@ -90,10 +90,10 @@
-
Statistik
+
  • - Zeichen pro Sekunde + Zeichen pro Minute (ZPM) 0
  • diff --git a/templates/statistics.html b/templates/statistics.html new file mode 100644 index 0000000..5b97bda --- /dev/null +++ b/templates/statistics.html @@ -0,0 +1,198 @@ + + + + + + Statistik - Typewriter Trainer + + + + + +
    +
    +
    +
    +
    +
    +

    Lektionsstatistik

    + +
    +
    +
    + +
    +
    Lektion auswählen
    +
    + {% for lesson in lessons %} +
    + {% if lesson_has_data[loop.index0] %} + + {% else %} + + {% endif %} +
    + {% endfor %} +
    +
    + + +
    + +
    +
    +
    +
    +
    +
    + + + + + + + diff --git a/zpm_test.py b/zpm_test.py new file mode 100644 index 0000000..fe0150c --- /dev/null +++ b/zpm_test.py @@ -0,0 +1,170 @@ +import sqlite3 +import time +import os +import sys + +def test_zpm_measurement_and_storage(): + """Testet die Messung und Speicherung von Zeichen pro Minute""" + + # Datenbankverbindung herstellen - erstelle Datenbank falls nicht vorhanden + db_path = 'typewriter.db' + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # Erstelle die Tabelle falls sie nicht existiert + cursor.execute(''' + CREATE TABLE IF NOT EXISTS lesson_statistic ( + id INTEGER PRIMARY KEY, + lesson_index INTEGER NOT NULL, + chars_per_minute FLOAT DEFAULT 0.0, + error_rate FLOAT DEFAULT 0.0, + wpm FLOAT DEFAULT 0.0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + ''') + conn.commit() + + try: + # Prüfe, ob die Tabelle lesson_statistic existiert + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='lesson_statistic'") + if not cursor.fetchone(): + print("Fehler: Tabelle 'lesson_statistic' existiert nicht!") + return False + + # Prüfe die Spalten der Tabelle + cursor.execute("PRAGMA table_info(lesson_statistic)") + columns = [column[1] for column in cursor.fetchall()] + expected_columns = ['id', 'lesson_index', 'chars_per_minute', 'error_rate', 'wpm', 'created_at'] + + if not all(col in columns for col in expected_columns): + print(f"Fehler: Tabelle hat nicht die erwarteten Spalten. Gefunden: {columns}") + return False + + print("✓ Datenbankstruktur ist korrekt") + + # Testdaten einfügen + test_lesson_index = 0 + test_chars_per_minute = 150 + test_error_rate = 5.5 + test_wpm = 30 + + cursor.execute(''' + INSERT INTO lesson_statistic (lesson_index, chars_per_minute, error_rate, wpm) + VALUES (?, ?, ?, ?) + ''', (test_lesson_index, test_chars_per_minute, test_error_rate, test_wpm)) + + conn.commit() + print("✓ Testdaten erfolgreich eingefügt") + + # Testdaten auslesen und überprüfen + cursor.execute(''' + SELECT lesson_index, chars_per_minute, error_rate, wpm + FROM lesson_statistic + WHERE lesson_index = ? + ORDER BY created_at DESC + LIMIT 1 + ''', (test_lesson_index,)) + + result = cursor.fetchone() + + if not result: + print("Fehler: Testdaten konnten nicht ausgelesen werden!") + return False + + stored_lesson_index, stored_chars_per_minute, stored_error_rate, stored_wpm = result + + # Überprüfe die gespeicherten Werte + if (stored_lesson_index == test_lesson_index and + stored_chars_per_minute == test_chars_per_minute and + stored_error_rate == test_error_rate and + stored_wpm == test_wpm): + print("✓ Alle Testwerte wurden korrekt gespeichert:") + print(f" Lektion: {stored_lesson_index}") + print(f" ZPM: {stored_chars_per_minute}") + print(f" Fehlerrate: {stored_error_rate}%") + print(f" WPM: {stored_wpm}") + else: + print("Fehler: Gespeicherte Werte stimmen nicht mit Testdaten überein!") + print(f" Erwartet: Lektion={test_lesson_index}, ZPM={test_chars_per_minute}, " + f"Fehlerrate={test_error_rate}, WPM={test_wpm}") + print(f" Gespeichert: Lektion={stored_lesson_index}, ZPM={stored_chars_per_minute}, " + f"Fehlerrate={stored_error_rate}, WPM={stored_wpm}") + return False + + # Teste die Berechnung von ZPM + print("\nTeste ZPM-Berechnung...") + + # Simuliere Tippen: 300 Zeichen in 60 Sekunden = 300 ZPM + test_chars = 300 + test_time_seconds = 60 + calculated_zpm = (test_chars / test_time_seconds) * 60 + + if calculated_zpm == 300: + print(f"✓ ZPM-Berechnung korrekt: {calculated_zpm} ZPM") + else: + print(f"Fehler: ZPM-Berechnung falsch. Erwartet: 300, Berechnet: {calculated_zpm}") + return False + + # Teste mehrere Datensätze für Zeitreihen + print("\nTeste Zeitreihen-Funktionalität...") + + # Füge mehrere Datensätze für dieselbe Lektion ein + test_data_points = [ + (test_lesson_index, 100, 10.0, 20), + (test_lesson_index, 150, 8.0, 25), + (test_lesson_index, 200, 5.5, 30) + ] + + for data_point in test_data_points: + cursor.execute(''' + INSERT INTO lesson_statistic (lesson_index, chars_per_minute, error_rate, wpm) + VALUES (?, ?, ?, ?) + ''', data_point) + + conn.commit() + + # Zähle die Datensätze für diese Lektion + cursor.execute(''' + SELECT COUNT(*) FROM lesson_statistic WHERE lesson_index = ? + ''', (test_lesson_index,)) + + count = cursor.fetchone()[0] + print(f"✓ Anzahl der Datensätze für Lektion {test_lesson_index}: {count}") + + # Hole die Zeitreihe + cursor.execute(''' + SELECT chars_per_minute, error_rate, wpm, created_at + FROM lesson_statistic + WHERE lesson_index = ? + ORDER BY created_at ASC + ''', (test_lesson_index,)) + + time_series = cursor.fetchall() + print(f"✓ Zeitreihe mit {len(time_series)} Datensätzen abgerufen") + + # Zeige die Entwicklung + print(" Entwicklung der ZPM-Werte:") + for i, (zpm, error_rate, wpm, created_at) in enumerate(time_series[-5:]): # Letzte 5 Einträge + print(f" Punkt {i+1}: ZPM={zpm}, Fehlerrate={error_rate}%, WPM={wpm}") + + return True + + except Exception as e: + print(f"Fehler während des Tests: {e}") + return False + finally: + conn.close() + +if __name__ == '__main__': + print("Starte ZPM-Test...") + print("=" * 50) + + success = test_zpm_measurement_and_storage() + + print("=" * 50) + if success: + print("🎉 Alle Tests erfolgreich! ZPM-Messung und -Speicherung funktionieren korrekt.") + sys.exit(0) + else: + print("❌ Tests fehlgeschlagen!") + sys.exit(1)