gateway/modules/features/commcoach/serviceCommcoachGamification.py
patrick-motsch 12b0d3d36e Alle 9 Fixes sind implementiert. Hier die Zusammenfassung:
Fix 1 -- Opening-Prompt: processSessionOpening in serviceCommcoach.py prüft jetzt ob es die erste Session ist (isFirstSession) und gibt der AI einen expliziten Prompt, der das Erfinden von Kontext verbietet.
Fix 2 -- Stabiler Transcript: onresult in CommcoachCoachingView.tsx nutzt jetzt processedResultIndexRef um nur neue Results zu verarbeiten. Finalisierte Teile werden stabil akkumuliert, kein Flackern mehr.
Fix 3 -- Hintergrundgeräusche-Timeout: Neuer silenceTimerRef mit 5s Timeout. Wenn nach onspeechstart kein Text kommt, wird isUserSpeaking automatisch zurückgesetzt.
Fix 4 -- Stop-Button: "Stop" Button erscheint im Session-Header wenn TTS läuft (via isTtsPlaying State, synchronisiert per 200ms Interval mit isTtsPlayingRef).
Fix 5 -- Weitersprechen-Button: lastTtsAudioRef speichert das zuletzt gespielte Audio. stopTts setzt wasInterrupted = true. "Weitersprechen" Button erscheint nach Unterbrechung und spielt das Audio erneut ab.
Fix 6 -- Paralleles TTS: Neue _generateAndEmitTts() Hilfsfunktion. In processMessage und processSessionOpening wird TTS als asyncio.create_task parallel zu _emitChunkedResponse gestartet.
Fix 7 -- JSON-Response: Die AI antwortet jetzt als JSON mit text, speech, documents. Neuer Prompt-Block wird in buildCoachingSystemPrompt angehängt. _parseAiJsonResponse() und _saveGeneratedDocument() im Backend. processMessage und processSessionOpening nutzen die neue Struktur.
Fix 8 -- Loading-States: Neuer actionLoading State in useCommcoach. Alle async Funktionen setzen setActionLoading('key') vor dem Await und null im finally. Buttons zeigen Loading-Text und werden disabled.
Fix 9 -- Umlaute: Alle deutschen Strings in allen CommCoach-Dateien (Backend + Frontend) korrigiert: ae->ä, oe->ö, ue->ü.
2026-03-04 22:53:41 +01:00

149 lines
4.8 KiB
Python

# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
CommCoach Gamification - Badge definitions and award logic.
Checks and awards badges after each session completion.
"""
import logging
from typing import Dict, Any, List, Optional
logger = logging.getLogger(__name__)
BADGE_DEFINITIONS: Dict[str, Dict[str, Any]] = {
"first_session": {
"label": "Erste Session",
"description": "Deine erste Coaching-Session abgeschlossen",
"icon": "star",
},
"streak_3": {
"label": "3-Tage-Serie",
"description": "3 Tage in Folge eine Session absolviert",
"icon": "fire",
},
"streak_7": {
"label": "Wochenserie",
"description": "7 Tage in Folge eine Session absolviert",
"icon": "fire",
},
"streak_30": {
"label": "Monatsserie",
"description": "30 Tage in Folge eine Session absolviert",
"icon": "fire",
},
"sessions_5": {
"label": "Engagiert",
"description": "5 Sessions abgeschlossen",
"icon": "trophy",
},
"sessions_10": {
"label": "Fortgeschritten",
"description": "10 Sessions abgeschlossen",
"icon": "trophy",
},
"sessions_25": {
"label": "Experte",
"description": "25 Sessions abgeschlossen",
"icon": "trophy",
},
"sessions_50": {
"label": "Meister",
"description": "50 Sessions abgeschlossen",
"icon": "trophy",
},
"high_score": {
"label": "Bestleistung",
"description": "Durchschnittsscore über 80 in einer Session",
"icon": "medal",
},
"multi_context": {
"label": "Vielseitig",
"description": "3 verschiedene Coaching-Themen aktiv",
"icon": "layers",
},
"roleplay_first": {
"label": "Rollenspieler",
"description": "Erste Roleplay-Session mit einer Persona abgeschlossen",
"icon": "theater",
},
"all_dimensions": {
"label": "Ganzheitlich",
"description": "In allen 5 Kompetenz-Dimensionen bewertet",
"icon": "compass",
},
"task_completer": {
"label": "Umsetzer",
"description": "10 Coaching-Aufgaben erledigt",
"icon": "check-circle",
},
}
async def checkAndAwardBadges(interface, userId: str, mandateId: str, instanceId: str,
session: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
"""Check badge conditions and award any newly earned badges. Returns list of newly awarded badges."""
awarded: List[Dict[str, Any]] = []
profile = interface.getProfile(userId, instanceId)
if not profile:
return awarded
totalSessions = profile.get("totalSessions", 0)
streakDays = profile.get("streakDays", 0)
badgesToCheck = [
("first_session", totalSessions >= 1),
("sessions_5", totalSessions >= 5),
("sessions_10", totalSessions >= 10),
("sessions_25", totalSessions >= 25),
("sessions_50", totalSessions >= 50),
("streak_3", streakDays >= 3),
("streak_7", streakDays >= 7),
("streak_30", streakDays >= 30),
]
if session and session.get("competenceScore"):
try:
score = float(session["competenceScore"])
if score >= 80:
badgesToCheck.append(("high_score", True))
except (ValueError, TypeError):
pass
if session and session.get("personaId") and session["personaId"] != "coach":
badgesToCheck.append(("roleplay_first", True))
try:
from .datamodelCommcoach import CoachingContextStatus
allContexts = interface.db.getRecordset(
interface.db.getRecordset.__self__.__class__.__mro__[0] # avoid import issues
) if False else []
except Exception:
allContexts = []
completedTasks = interface.getCompletedTaskCount(userId) if hasattr(interface, 'getCompletedTaskCount') else 0
if completedTasks >= 10:
badgesToCheck.append(("task_completer", True))
for badgeKey, condition in badgesToCheck:
if condition and not interface.hasBadge(userId, instanceId, badgeKey):
badgeData = {
"userId": userId,
"mandateId": mandateId,
"instanceId": instanceId,
"badgeKey": badgeKey,
}
newBadge = interface.awardBadge(badgeData)
definition = BADGE_DEFINITIONS.get(badgeKey, {})
newBadge["label"] = definition.get("label", badgeKey)
newBadge["description"] = definition.get("description", "")
newBadge["icon"] = definition.get("icon", "star")
awarded.append(newBadge)
logger.info(f"Badge '{badgeKey}' awarded to user {userId}")
return awarded
def getBadgeDefinitions() -> Dict[str, Dict[str, Any]]:
"""Return all badge definitions for the frontend."""
return BADGE_DEFINITIONS