-m "Auf ZPM umgestellt"

This commit is contained in:
jamulix 2025-10-24 07:26:13 +02:00 committed by jamulix (aider)
commit 80c40ab983
8 changed files with 494 additions and 15 deletions

87
app.py
View file

@ -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/<int:lesson_index>')
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"""

View file

@ -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,

View file

@ -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())

View file

@ -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;

View file

@ -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]);

View file

@ -90,10 +90,10 @@
<div class="col-md-6">
<div class="card bg-light">
<div class="card-body">
<h6 class="card-title">Statistik</h6>
<h6 class="card-title statistics-link">Statistik</h6>
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between align-items-center">
Zeichen pro Sekunde
Zeichen pro Minute (ZPM)
<span id="speed" class="badge bg-primary rounded-pill">0</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">

198
templates/statistics.html Normal file
View file

@ -0,0 +1,198 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Statistik - Typewriter Trainer</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body class="bg-light" data-bs-theme="light">
<div class="container py-4">
<div class="row justify-content-center">
<div class="col-md-10">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<div class="d-flex justify-content-between align-items-center">
<h2 class="mb-0">Lektionsstatistik</h2>
<button onclick="window.location.href='/'" class="btn btn-outline-light">
Zurück zum Training
</button>
</div>
</div>
<div class="card-body">
<!-- Lektionen Auswahl -->
<div class="mb-4">
<h5 class="card-title">Lektion auswählen</h5>
<div class="d-flex flex-wrap justify-content-start" id="lessons-container">
{% for lesson in lessons %}
<div class="mb-1 me-1" style="width: 40px;">
{% if lesson_has_data[loop.index0] %}
<div class="lesson-link {% if loop.index0 == current_lesson_index %}fw-bold text-danger{% else %}text-primary{% endif %}"
style="cursor: pointer; font-size: 0.9rem; text-align: center; white-space: nowrap; min-height: 24px; display: flex; align-items: center; justify-content: center;"
onclick="loadLessonStatistics({{ loop.index0 }})"
data-lesson-index="{{ loop.index0 }}">
{{ lesson.lesson if lesson.lesson is defined else loop.index0 + 1 }}
</div>
{% else %}
<div class="lesson-link text-muted"
style="font-size: 0.9rem; text-align: center; white-space: nowrap; min-height: 24px; display: flex; align-items: center; justify-content: center;"
data-lesson-index="{{ loop.index0 }}">
{{ lesson.lesson if lesson.lesson is defined else loop.index0 + 1 }}
</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
<!-- Diagramm -->
<div class="mb-4">
<canvas id="statisticsChart" width="400" height="200"></canvas>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
let currentChart = null;
function loadLessonStatistics(lessonIndex) {
// Aktualisiere die Lektionsmarkierung
document.querySelectorAll('.lesson-link').forEach(link => {
const index = parseInt(link.getAttribute('data-lesson-index'));
if (index === lessonIndex) {
link.classList.add('fw-bold', 'text-danger');
link.classList.remove('text-primary');
} else {
link.classList.remove('fw-bold', 'text-danger');
link.classList.add('text-primary');
}
});
// Lade die Statistiken für die ausgewählte Lektion
fetch(`/get_lesson_statistics/${lessonIndex}`)
.then(response => response.json())
.then(data => {
if (data.error) {
console.error('Fehler:', data.error);
return;
}
updateChart(data);
})
.catch(error => console.error('Fehler:', error));
}
function updateChart(data) {
const ctx = document.getElementById('statisticsChart').getContext('2d');
// Zerstöre vorheriges Diagramm falls vorhanden
if (currentChart) {
currentChart.destroy();
}
currentChart = new Chart(ctx, {
type: 'line',
data: {
labels: data.dates,
datasets: [
{
label: 'ZPM (Zeichen pro Minute)',
data: data.chars_per_minute,
borderColor: 'blue',
backgroundColor: 'rgba(0, 0, 255, 0.1)',
tension: 0.1,
yAxisID: 'y'
},
{
label: 'Err (Fehler in %)',
data: data.error_rate,
borderColor: 'red',
backgroundColor: 'rgba(255, 0, 0, 0.1)',
tension: 0.1,
yAxisID: 'y1'
},
{
label: 'WPM (Wörter pro Minute)',
data: data.wpm,
borderColor: 'green',
backgroundColor: 'rgba(0, 255, 0, 0.1)',
tension: 0.1,
yAxisID: 'y2'
}
]
},
options: {
responsive: true,
interaction: {
mode: 'index',
intersect: false,
},
scales: {
x: {
title: {
display: true,
text: 'Datum (YY-MM-DD)'
},
ticks: {
maxRotation: 90,
minRotation: 90
}
},
y: {
type: 'linear',
display: true,
position: 'left',
title: {
display: true,
text: 'ZPM'
},
min: 0,
suggestedMax: Math.max(...data.chars_per_minute) * 1.1 || 1
},
y1: {
type: 'linear',
display: true,
position: 'right',
title: {
display: true,
text: 'Err (%)'
},
min: 0,
suggestedMax: Math.max(...data.error_rate) * 1.1 || 1,
grid: {
drawOnChartArea: false,
},
},
y2: {
type: 'linear',
display: true,
position: 'right',
title: {
display: true,
text: 'WPM'
},
min: 0,
suggestedMax: Math.max(...data.wpm) * 1.1 || 1,
grid: {
drawOnChartArea: false,
},
}
}
}
});
}
// Lade initial die Statistiken für die aktuelle Lektion
document.addEventListener('DOMContentLoaded', function() {
loadLessonStatistics({{ current_lesson_index }});
});
</script>
</body>
</html>

170
zpm_test.py Normal file
View file

@ -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)