This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/frontend/modules/companion.py
Benjamin Admin 21a844cb8a fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

771 lines
20 KiB
Python

"""
Companion Dashboard Module - Begleiter-Modus UI.
Das Dashboard zeigt:
- Aktuelle Phase im Schuljahr
- Priorisierte Vorschläge
- Fortschritts-Anzeige
- Kommende Termine
"""
def get_companion_css() -> str:
"""CSS für das Companion Dashboard."""
return """
/* Companion Dashboard Styles */
.companion-container {
max-width: 900px;
margin: 0 auto;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.companion-header {
text-align: center;
margin-bottom: 30px;
}
.companion-header h1 {
font-size: 28px;
color: #1a1a2e;
margin-bottom: 8px;
}
.companion-header .phase-badge {
display: inline-block;
background: linear-gradient(135deg, #6C1B1B, #8B2525);
color: white;
padding: 6px 16px;
border-radius: 20px;
font-size: 14px;
font-weight: 500;
}
/* Phase Indicator */
.phase-indicator {
background: #f8f9fa;
border-radius: 12px;
padding: 20px;
margin-bottom: 24px;
}
.phase-timeline {
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
padding: 0 10px;
}
.phase-timeline::before {
content: '';
position: absolute;
top: 15px;
left: 30px;
right: 30px;
height: 3px;
background: #e0e0e0;
z-index: 0;
}
.phase-step {
display: flex;
flex-direction: column;
align-items: center;
z-index: 1;
cursor: pointer;
}
.phase-dot {
width: 30px;
height: 30px;
border-radius: 50%;
background: #e0e0e0;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
transition: all 0.3s ease;
}
.phase-dot.completed {
background: #22c55e;
}
.phase-dot.current {
background: #6C1B1B;
box-shadow: 0 0 0 4px rgba(108, 27, 27, 0.2);
}
.phase-dot .material-icons {
font-size: 16px;
color: white;
}
.phase-label {
font-size: 11px;
color: #666;
text-align: center;
max-width: 70px;
}
.phase-label.current {
color: #6C1B1B;
font-weight: 600;
}
/* Suggestions List */
.suggestions-section {
margin-bottom: 24px;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: #1a1a2e;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.suggestion-card {
display: flex;
align-items: center;
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
transition: all 0.2s ease;
cursor: pointer;
}
.suggestion-card:hover {
border-color: #6C1B1B;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
transform: translateY(-1px);
}
.priority-bar {
width: 4px;
height: 50px;
border-radius: 2px;
margin-right: 16px;
flex-shrink: 0;
}
.priority-bar.urgent { background: #ef4444; }
.priority-bar.high { background: #f97316; }
.priority-bar.medium { background: #3b82f6; }
.priority-bar.low { background: #9ca3af; }
.suggestion-icon {
width: 40px;
height: 40px;
border-radius: 10px;
background: #f3f4f6;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
flex-shrink: 0;
}
.suggestion-icon .material-icons {
color: #6C1B1B;
}
.suggestion-content {
flex: 1;
}
.suggestion-title {
font-weight: 600;
color: #1a1a2e;
margin-bottom: 4px;
}
.suggestion-description {
font-size: 13px;
color: #6b7280;
}
.suggestion-action {
color: #6C1B1B;
font-weight: 600;
font-size: 14px;
display: flex;
align-items: center;
gap: 4px;
}
.suggestion-time {
font-size: 12px;
color: #9ca3af;
margin-top: 4px;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 40px 20px;
background: #f0fdf4;
border-radius: 12px;
}
.empty-state .material-icons {
font-size: 48px;
color: #22c55e;
margin-bottom: 12px;
}
.empty-state h3 {
color: #166534;
margin-bottom: 8px;
}
.empty-state p {
color: #6b7280;
}
/* Stats Cards */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
margin-bottom: 24px;
}
.stat-card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 10px;
padding: 16px;
text-align: center;
}
.stat-value {
font-size: 28px;
font-weight: 700;
color: #6C1B1B;
}
.stat-label {
font-size: 12px;
color: #6b7280;
margin-top: 4px;
}
/* Progress Card */
.progress-card {
background: #eff6ff;
border-radius: 12px;
padding: 20px;
margin-bottom: 24px;
}
.progress-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.progress-title {
font-weight: 600;
color: #1e40af;
}
.progress-percentage {
font-weight: 700;
color: #1e40af;
}
.progress-bar-container {
background: #dbeafe;
border-radius: 10px;
height: 10px;
overflow: hidden;
}
.progress-bar-fill {
background: linear-gradient(90deg, #3b82f6, #1d4ed8);
height: 100%;
border-radius: 10px;
transition: width 0.5s ease;
}
.progress-milestones {
margin-top: 12px;
font-size: 13px;
color: #4b5563;
}
/* Events Card */
.events-card {
background: #fefce8;
border-radius: 12px;
padding: 20px;
}
.event-item {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #fef08a;
}
.event-item:last-child {
border-bottom: none;
}
.event-icon {
width: 36px;
height: 36px;
border-radius: 8px;
background: white;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
}
.event-icon .material-icons {
color: #ca8a04;
font-size: 20px;
}
.event-info {
flex: 1;
}
.event-title {
font-weight: 500;
color: #1a1a2e;
}
.event-date {
font-size: 12px;
color: #6b7280;
}
.event-badge {
font-size: 12px;
font-weight: 600;
color: #ca8a04;
background: white;
padding: 4px 10px;
border-radius: 12px;
}
/* Mode Toggle */
.mode-toggle {
display: flex;
background: #f3f4f6;
border-radius: 8px;
padding: 4px;
margin-bottom: 20px;
}
.mode-btn {
flex: 1;
padding: 10px 16px;
border: none;
background: transparent;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.mode-btn.active {
background: #6C1B1B;
color: white;
}
.mode-btn:not(.active):hover {
background: #e5e7eb;
}
/* Responsive */
@media (max-width: 600px) {
.phase-timeline {
overflow-x: auto;
padding-bottom: 10px;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.suggestion-card {
flex-wrap: wrap;
}
}
"""
def get_companion_html() -> str:
"""HTML Template für das Companion Dashboard."""
return """
<div class="companion-container" id="companionDashboard">
<!-- Mode Toggle -->
<div class="mode-toggle">
<button class="mode-btn active" id="modeCompanion" onclick="setMode('companion')">
<span class="material-icons" style="font-size: 16px; vertical-align: middle; margin-right: 4px;">assistant</span>
Begleiter
</button>
<button class="mode-btn" id="modeClassic" onclick="setMode('classic')">
<span class="material-icons" style="font-size: 16px; vertical-align: middle; margin-right: 4px;">dashboard</span>
Klassisch
</button>
</div>
<!-- Header -->
<div class="companion-header">
<h1>Was ist jetzt wichtig?</h1>
<span class="phase-badge" id="phaseBadge">Lädt...</span>
</div>
<!-- Phase Indicator -->
<div class="phase-indicator">
<div class="phase-timeline" id="phaseTimeline">
<!-- Dynamisch gefüllt -->
</div>
</div>
<!-- Stats Grid -->
<div class="stats-grid" id="statsGrid">
<div class="stat-card">
<div class="stat-value" id="statClasses">0</div>
<div class="stat-label">Klassen</div>
</div>
<div class="stat-card">
<div class="stat-value" id="statStudents">0</div>
<div class="stat-label">Schüler</div>
</div>
<div class="stat-card">
<div class="stat-value" id="statUnits">0</div>
<div class="stat-label">Lerneinheiten</div>
</div>
<div class="stat-card">
<div class="stat-value" id="statGrades">0</div>
<div class="stat-label">Noten</div>
</div>
</div>
<!-- Progress Card -->
<div class="progress-card" id="progressCard">
<div class="progress-header">
<span class="progress-title">Fortschritt in dieser Phase</span>
<span class="progress-percentage" id="progressPercent">0%</span>
</div>
<div class="progress-bar-container">
<div class="progress-bar-fill" id="progressBar" style="width: 0%"></div>
</div>
<div class="progress-milestones" id="progressMilestones">
0 von 0 Meilensteinen erreicht
</div>
</div>
<!-- Suggestions -->
<div class="suggestions-section">
<div class="section-title">
<span class="material-icons">lightbulb</span>
Empfohlene Aktionen
</div>
<div id="suggestionsList">
<!-- Dynamisch gefüllt -->
</div>
</div>
<!-- Events -->
<div class="events-card" id="eventsCard" style="display: none;">
<div class="section-title" style="margin-bottom: 12px;">
<span class="material-icons">event</span>
Kommende Termine
</div>
<div id="eventsList">
<!-- Dynamisch gefüllt -->
</div>
</div>
</div>
"""
def get_companion_js() -> str:
"""JavaScript für das Companion Dashboard."""
return """
// Companion Dashboard JavaScript
let companionData = null;
let currentMode = 'companion';
async function loadCompanionDashboard() {
try {
const response = await fetch('/api/state/dashboard?teacher_id=demo-teacher');
companionData = await response.json();
renderDashboard();
} catch (error) {
console.error('Error loading dashboard:', error);
showError('Dashboard konnte nicht geladen werden');
}
}
function renderDashboard() {
if (!companionData) return;
// Phase Badge
document.getElementById('phaseBadge').textContent = companionData.context.phase_display_name;
// Phase Timeline
renderPhaseTimeline();
// Stats
document.getElementById('statClasses').textContent = companionData.stats.classes_count || 0;
document.getElementById('statStudents').textContent = companionData.stats.students_count || 0;
document.getElementById('statUnits').textContent = companionData.stats.learning_units_created || 0;
document.getElementById('statGrades').textContent = companionData.stats.grades_entered || 0;
// Progress
const progress = companionData.progress;
document.getElementById('progressPercent').textContent = Math.round(progress.percentage) + '%';
document.getElementById('progressBar').style.width = progress.percentage + '%';
document.getElementById('progressMilestones').textContent =
`${progress.completed} von ${progress.total} Meilensteinen erreicht`;
// Suggestions
renderSuggestions();
// Events
renderEvents();
}
function renderPhaseTimeline() {
const container = document.getElementById('phaseTimeline');
container.innerHTML = '';
companionData.phases.forEach(phase => {
const step = document.createElement('div');
step.className = 'phase-step';
step.onclick = () => console.log('Phase clicked:', phase.phase);
const dot = document.createElement('div');
dot.className = 'phase-dot';
if (phase.is_completed) {
dot.classList.add('completed');
dot.innerHTML = '<span class="material-icons">check</span>';
} else if (phase.is_current) {
dot.classList.add('current');
dot.innerHTML = '<span class="material-icons">circle</span>';
}
const label = document.createElement('div');
label.className = 'phase-label';
if (phase.is_current) label.classList.add('current');
label.textContent = phase.short_name;
step.appendChild(dot);
step.appendChild(label);
container.appendChild(step);
});
}
function renderSuggestions() {
const container = document.getElementById('suggestionsList');
if (!companionData.suggestions || companionData.suggestions.length === 0) {
container.innerHTML = `
<div class="empty-state">
<span class="material-icons">check_circle</span>
<h3>Alles erledigt!</h3>
<p>Keine offenen Aufgaben. Gute Arbeit!</p>
</div>
`;
return;
}
container.innerHTML = companionData.suggestions.map(s => `
<div class="suggestion-card" onclick="navigateTo('${s.action_target}')">
<div class="priority-bar ${s.priority.toLowerCase()}"></div>
<div class="suggestion-icon">
<span class="material-icons">${s.icon}</span>
</div>
<div class="suggestion-content">
<div class="suggestion-title">${s.title}</div>
<div class="suggestion-description">${s.description}</div>
<div class="suggestion-time">
<span class="material-icons" style="font-size: 14px; vertical-align: middle;">schedule</span>
ca. ${s.estimated_time} Min.
</div>
</div>
<div class="suggestion-action">
Los <span class="material-icons" style="font-size: 18px;">arrow_forward</span>
</div>
</div>
`).join('');
}
function renderEvents() {
const container = document.getElementById('eventsList');
const card = document.getElementById('eventsCard');
if (!companionData.upcoming_events || companionData.upcoming_events.length === 0) {
card.style.display = 'none';
return;
}
card.style.display = 'block';
const getEventIcon = (type) => {
const icons = {
'exam': 'quiz',
'parent_meeting': 'groups',
'deadline': 'alarm',
'default': 'event'
};
return icons[type] || icons.default;
};
container.innerHTML = companionData.upcoming_events.map(e => `
<div class="event-item">
<div class="event-icon">
<span class="material-icons">${getEventIcon(e.type)}</span>
</div>
<div class="event-info">
<div class="event-title">${e.title}</div>
<div class="event-date">${formatDate(e.date)}</div>
</div>
<div class="event-badge">
${e.in_days === 0 ? 'Heute' : `In ${e.in_days} Tagen`}
</div>
</div>
`).join('');
}
function formatDate(isoString) {
const date = new Date(isoString);
return date.toLocaleDateString('de-DE', {
weekday: 'short',
day: 'numeric',
month: 'short'
});
}
function navigateTo(target) {
console.log('Navigate to:', target);
// In echter App: window.location.href = target;
// Oder: router.push(target);
// Für Demo: Zeige Nachricht
showToast(`Navigiere zu: ${target}`);
}
function setMode(mode) {
currentMode = mode;
document.getElementById('modeCompanion').classList.toggle('active', mode === 'companion');
document.getElementById('modeClassic').classList.toggle('active', mode === 'classic');
if (mode === 'classic') {
showToast('Klassischer Modus - Navigation zu Dashboard');
// window.location.href = '/studio';
}
}
function showToast(message) {
// Einfache Toast-Nachricht
const toast = document.createElement('div');
toast.style.cssText = `
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: #1a1a2e;
color: white;
padding: 12px 24px;
border-radius: 8px;
z-index: 1000;
animation: fadeIn 0.3s ease;
`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 300);
}, 2000);
}
function showError(message) {
document.getElementById('suggestionsList').innerHTML = `
<div class="empty-state" style="background: #fef2f2;">
<span class="material-icons" style="color: #ef4444;">error</span>
<h3 style="color: #b91c1c;">Fehler</h3>
<p>${message}</p>
</div>
`;
}
// Milestone abschließen
async function completeMilestone(milestone) {
try {
const response = await fetch('/api/state/milestone?teacher_id=demo-teacher', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ milestone })
});
const result = await response.json();
if (result.success) {
showToast(`Meilenstein "${milestone}" abgeschlossen!`);
if (result.new_phase) {
showToast(`Neue Phase: ${result.new_phase}`);
}
loadCompanionDashboard(); // Reload
}
} catch (error) {
console.error('Error completing milestone:', error);
}
}
// Initial laden
document.addEventListener('DOMContentLoaded', loadCompanionDashboard);
"""
class CompanionModule:
"""
Companion Dashboard Module für den Begleiter-Modus.
Zeigt:
- Aktuelle Phase im Schuljahr
- Priorisierte Vorschläge
- Fortschritts-Anzeige
- Kommende Termine
"""
def __init__(self):
self.name = "companion"
self.display_name = "Begleiter"
self.icon = "assistant"
def get_css(self) -> str:
return get_companion_css()
def get_html(self) -> str:
return get_companion_html()
def get_js(self) -> str:
return get_companion_js()
def render(self) -> dict:
"""Rendert das komplette Modul."""
return {
"css": self.get_css(),
"html": self.get_html(),
"js": self.get_js(),
}