From dbe71dcb971a1cdc99e8569d141cca5ecfd612da Mon Sep 17 00:00:00 2001 From: Ronny Date: Sat, 11 Apr 2026 18:56:48 +0200 Subject: [PATCH] =?UTF-8?q?refactor:=20simplify=20TipModal=20=E2=80=94=20r?= =?UTF-8?q?emove=20Expertenblick=20and=20redundant=20header?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Strips all insight/agent state, fetchInsight() SSE function, audio playback logic, and the insightWrapper JSX block that called /api/agent/* routes. Also removes the matchHeader/groupBadge and kickoffBlock from the modal (info already visible on the match card). Cleans all corresponding CSS. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/components/TipModal.module.css | 304 +------------------- frontend/src/components/TipModal.tsx | 232 +-------------- 2 files changed, 4 insertions(+), 532 deletions(-) diff --git a/frontend/src/components/TipModal.module.css b/frontend/src/components/TipModal.module.css index 95538c2..aeb16be 100644 --- a/frontend/src/components/TipModal.module.css +++ b/frontend/src/components/TipModal.module.css @@ -57,27 +57,6 @@ margin: 0 auto 20px; } -/* Match header */ -.matchHeader { - display: flex; - align-items: center; - justify-content: center; - margin-bottom: 24px; -} - -.groupBadge { - font-size: 11px; - font-weight: 700; - color: var(--primary); - background: var(--primary-dim); - padding: 3px 10px; - border-radius: 20px; - border: 1px solid rgba(75,183,248,0.2); - text-transform: uppercase; - letter-spacing: 0.05em; -} - - /* Teams row */ .teamsRow { display: grid; @@ -141,13 +120,6 @@ line-height: 1.2; } -.teamShort { - font-size: 11px; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.08em; -} - .vsBlock { display: flex; align-items: center; @@ -155,31 +127,6 @@ height: 72px; } -.kickoffBlock { - display: flex; - flex-direction: column; - align-items: center; - gap: 2px; -} - -.kickoffDate { - font-family: 'Plus Jakarta Sans', sans-serif; - font-size: 11px; - font-weight: 700; - color: var(--text-secondary); - text-align: center; - white-space: nowrap; -} - -.kickoffTime { - font-family: 'Plus Jakarta Sans', sans-serif; - font-size: 12px; - font-weight: 800; - color: var(--primary); - text-align: center; - white-space: nowrap; -} - /* Picker section */ .pickerSection { margin-bottom: 20px; @@ -241,7 +188,7 @@ inset 0 -1px 0 rgba(0,0,0,0.06); } -/* Glossy sheen – gleich wie flagLarge */ +/* Glossy sheen */ .pickerBtn::before { content: ''; position: absolute; @@ -297,7 +244,7 @@ inset 1px 0 0 rgba(255,255,255,0.05); } -/* Glossy sheen – gleich wie flagLarge und pickerBtn */ +/* Glossy sheen */ .tendencyBar::before { content: ''; position: absolute; @@ -319,253 +266,6 @@ font-weight: 700; } -/* ---- Expertenblick ---- */ -.insightWrapper { - margin-bottom: 20px; -} - -/* Toggle-Zeile mit Play-Button */ -.insightToggleRow { - display: flex; - align-items: center; - gap: 8px; -} - -.insightToggleRow .insightToggle { - flex: 1; -} - -/* Audio-Play-Button */ -.audioBtn { - display: flex; - align-items: center; - gap: 5px; - padding: 9px 12px; - background: linear-gradient(135deg, rgba(75,183,248,0.12) 0%, rgba(75,183,248,0.05) 100%); - border: 1px solid rgba(75,183,248,0.3); - border-radius: 12px; - color: var(--cyan); - font-size: 12px; - font-weight: 700; - font-family: 'Plus Jakarta Sans', sans-serif; - cursor: pointer; - transition: all 0.18s; - white-space: nowrap; - flex-shrink: 0; -} - -.audioBtn:hover:not(:disabled) { - background: linear-gradient(135deg, rgba(75,183,248,0.2) 0%, rgba(75,183,248,0.08) 100%); - border-color: rgba(75,183,248,0.5); -} - -.audioBtn:disabled { - opacity: 0.6; - cursor: default; -} - -/* Dialog-Format: Delling / Netzer */ -.dialogLine { - display: flex; - flex-direction: column; - gap: 2px; - padding: 8px 12px; - border-radius: 10px; - margin-bottom: 6px; - animation: insightFadeIn 0.2s ease; -} - -.dialogLine:last-child { - margin-bottom: 0; -} - -.speakerDelling { - background: rgba(148, 163, 184, 0.06); - border-left: 2px solid rgba(148,163,184,0.4); - align-items: flex-start; -} - -.speakerNetzer { - background: rgba(254, 174, 50, 0.06); - border-left: 2px solid rgba(254,174,50,0.5); - align-items: flex-start; -} - -.dialogSpeaker { - font-size: 10px; - font-weight: 800; - letter-spacing: 0.08em; - text-transform: uppercase; - margin-bottom: 2px; -} - -.speakerDelling .dialogSpeaker { - color: rgba(148,163,184,0.8); -} - -.speakerNetzer .dialogSpeaker { - color: var(--gold); -} - -.dialogText { - font-size: 13px; - line-height: 1.55; - color: var(--text-primary); - font-style: italic; -} - -.insightToggle { - width: 100%; - display: flex; - align-items: center; - gap: 8px; - padding: 11px 16px; - background: linear-gradient(135deg, rgba(254,174,50,0.12) 0%, rgba(254,174,50,0.05) 100%); - border: 1px solid rgba(254,174,50,0.3); - border-radius: 12px; - color: var(--gold); - font-size: 13px; - font-weight: 700; - font-family: 'Plus Jakarta Sans', sans-serif; - cursor: pointer; - transition: all 0.18s; - text-align: left; - letter-spacing: 0.02em; - position: relative; - overflow: hidden; - box-shadow: - 0 2px 12px rgba(254,174,50,0.1), - inset 0 1px 0 rgba(254,174,50,0.15); -} - -/* Glossy sheen */ -.insightToggle::before { - content: ''; - position: absolute; - top: 0; left: 0; right: 0; - height: 50%; - background: linear-gradient(180deg, rgba(255,255,255,0.06) 0%, transparent 100%); - pointer-events: none; -} - -.insightToggle:hover { - background: linear-gradient(135deg, rgba(254,174,50,0.2) 0%, rgba(254,174,50,0.08) 100%); - border-color: rgba(254,174,50,0.5); - box-shadow: - 0 4px 20px rgba(254,174,50,0.2), - inset 0 1px 0 rgba(254,174,50,0.2); -} - -.insightIcon { - color: var(--gold); - flex-shrink: 0; - filter: drop-shadow(0 0 4px rgba(254,174,50,0.5)); -} - -.insightChevron { - margin-left: auto; - color: rgba(254,174,50,0.6); - transition: transform 0.2s; -} - -.insightPanel { - margin-top: 8px; - padding: 14px 16px; - background: var(--surface-high); - border-radius: 12px; - border: 1px solid rgba(254,174,50,0.1); - box-shadow: inset 0 1px 0 rgba(254,174,50,0.05); - animation: insightFadeIn 0.2s ease; -} - -@keyframes insightFadeIn { - from { opacity: 0; transform: translateY(-4px); } - to { opacity: 1; transform: translateY(0); } -} - -.insightLoading { - display: flex; - align-items: center; - gap: 8px; - color: var(--text-muted); - font-size: 13px; -} - -.insightSpinner { - color: var(--primary); - animation: spin 1s linear infinite; -} - -@keyframes spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } -} - -.insightText { - display: flex; - flex-direction: column; - gap: 0; -} - -.insightLine { - display: flex; - flex-direction: column; - gap: 3px; - padding: 10px 0; - border-bottom: 1px solid var(--border-subtle); -} - -.insightLine:last-child { - border-bottom: none; - padding-bottom: 0; -} - -.insightLine:first-child { - padding-top: 0; -} - -.insightLabel { - font-family: 'Plus Jakarta Sans', sans-serif; - font-weight: 700; - font-size: 10px; - color: var(--primary); - text-transform: uppercase; - letter-spacing: 0.08em; -} - -.insightValue { - font-size: 13px; - line-height: 1.5; - color: var(--text-secondary); -} - -.insightValue strong { - color: var(--text-primary); - font-weight: 700; -} - -.insightCursor { - display: inline-block; - width: 2px; - height: 14px; - background: var(--primary); - border-radius: 1px; - margin-left: 2px; - vertical-align: middle; - animation: blink 0.8s ease-in-out infinite; -} - -@keyframes blink { - 0%, 100% { opacity: 1; } - 50% { opacity: 0; } -} - -.insightErrorMsg { - font-size: 13px; - color: var(--text-muted); - text-align: center; -} - /* Error */ .error { color: var(--error); diff --git a/frontend/src/components/TipModal.tsx b/frontend/src/components/TipModal.tsx index 1e206e9..6ad61a7 100644 --- a/frontend/src/components/TipModal.tsx +++ b/frontend/src/components/TipModal.tsx @@ -1,5 +1,4 @@ -import { useState, useRef } from 'react'; -import { Sparkles, ChevronDown, ChevronUp, Loader2, Volume2, Square } from 'lucide-react'; +import { useState } from 'react'; import { Match, api } from '../api/client'; import styles from './TipModal.module.css'; @@ -17,53 +16,6 @@ function getTendency(home: number, away: number): Tendency { return 'draw'; } -// Streaming-Fetch für /api/agent/insight -async function fetchInsight( - homeTeam: string, - awayTeam: string, - stage: string, - group: string | null, - onChunk: (text: string) => void, - onDone: () => void, - onError: () => void -) { - try { - const res = await fetch('/api/agent/insight', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ homeTeam, awayTeam, stage, group }), - }); - if (!res.ok) { onError(); return; } - - const reader = res.body?.getReader(); - if (!reader) { onError(); return; } - - const decoder = new TextDecoder(); - let buffer = ''; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split('\n'); - buffer = lines.pop() ?? ''; - for (const line of lines) { - if (!line.startsWith('data: ')) continue; - const payload = line.slice(6).trim(); - if (payload === '[DONE]') { onDone(); return; } - try { - const parsed = JSON.parse(payload); - if (parsed.text) onChunk(parsed.text); - if (parsed.error) { onError(); return; } - } catch { /* ignore */ } - } - } - onDone(); - } catch { - onError(); - } -} - export default function TipModal({ match, onClose, onSaved }: Props) { const existing = match.userTip; const [home, setHome] = useState(existing?.home ?? 0); @@ -71,78 +23,6 @@ export default function TipModal({ match, onClose, onSaved }: Props) { const [saving, setSaving] = useState(false); const [error, setError] = useState(null); - // Expertenblick State - const [insightOpen, setInsightOpen] = useState(false); - const [insightText, setInsightText] = useState(''); - const [insightLoading, setInsightLoading] = useState(false); - const [insightError, setInsightError] = useState(false); - const insightFetched = useRef(false); - - // Audio State - const [audioLoading, setAudioLoading] = useState(false); - const [audioPlaying, setAudioPlaying] = useState(false); - const [audioError, setAudioError] = useState(false); - const audioRef = useRef(null); - - async function handlePlayAudio() { - // Stop wenn gerade läuft - if (audioPlaying && audioRef.current) { - audioRef.current.pause(); - audioRef.current.currentTime = 0; - setAudioPlaying(false); - return; - } - setAudioError(false); - setAudioLoading(true); - try { - const res = await fetch('/api/agent/insight-audio', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ dialogText: insightText }), - }); - if (!res.ok) throw new Error('Audio-Generierung fehlgeschlagen'); - const blob = await res.blob(); - const url = URL.createObjectURL(blob); - const audio = new Audio(url); - audioRef.current = audio; - audio.onended = () => { - setAudioPlaying(false); - URL.revokeObjectURL(url); - }; - audio.onerror = () => { - setAudioPlaying(false); - setAudioError(true); - }; - setAudioLoading(false); - setAudioPlaying(true); - await audio.play(); - } catch { - setAudioLoading(false); - setAudioError(true); - } - } - - function handleToggleInsight() { - const opening = !insightOpen; - setInsightOpen(opening); - // Nur einmal laden - if (opening && !insightFetched.current) { - insightFetched.current = true; - setInsightLoading(true); - setInsightText(''); - setInsightError(false); - fetchInsight( - match.homeTeam.name, - match.awayTeam.name, - match.stage, - match.group, - (chunk) => setInsightText((t) => t + chunk), - () => setInsightLoading(false), - () => { setInsightLoading(false); setInsightError(true); } - ); - } - } - const tendency = getTendency(home, away); const tendencyLabel = tendency === 'home' ? match.homeTeam.shortName || match.homeTeam.name : @@ -172,15 +52,6 @@ export default function TipModal({ match, onClose, onSaved }: Props) { {/* Drag handle */}
- {/* Match info header */} -
- {match.group && ( - - {match.group.replace('GROUP_', 'Gruppe ')} - - )} -
- {/* Teams mit Flaggen */}
@@ -193,21 +64,7 @@ export default function TipModal({ match, onClose, onSaved }: Props) { {match.homeTeam.name}
-
-
- - {new Date(match.utcDate).toLocaleString('de-DE', { - weekday: 'short', day: 'numeric', month: 'short', - timeZone: 'Europe/Berlin' - })} - - - {new Date(match.utcDate).toLocaleString('de-DE', { - hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Berlin' - })} Uhr - -
-
+
@@ -240,91 +97,6 @@ export default function TipModal({ match, onClose, onSaved }: Props) {
- {/* Expertenblick */} -
-
- - - {/* Play-Button – nur wenn Dialog fertig geladen */} - {insightOpen && !insightLoading && !insightError && insightText && ( - - )} -
- - {insightOpen && ( -
- {insightLoading && insightText === '' ? ( -
- - Analyse läuft… -
- ) : insightError ? ( -
- Einschätzung gerade nicht verfügbar. -
- ) : ( -
- {insightText.split('\n').filter(l => l.trim()).map((line, i) => { - // Dialog-Format: **Delling:** "..." oder **Netzer:** "..." - const dialogMatch = line.match(/^\*\*(Delling|Netzer):\*\*\s*["„]?(.+?)[""]?\s*$/); - if (dialogMatch) { - const speaker = dialogMatch[1] as 'Delling' | 'Netzer'; - return ( -
- {speaker} - „{dialogMatch[2].replace(/^["„]|[""]$/g, '')}" -
- ); - } - // Fallback: **Label:** Value (alter Stil) - const labelMatch = line.match(/^\*\*(.+?):\*\*\s*(.*)$/); - if (labelMatch) { - const valueParts = labelMatch[2].split(/(\*\*.+?\*\*)/g).map((part, j) => - part.startsWith('**') && part.endsWith('**') - ? {part.slice(2, -2)} - : part - ); - return ( -
- {labelMatch[1]} - {valueParts} -
- ); - } - return
{line}
; - })} - {insightLoading && } - {audioError && ( -
- Audio nicht verfügbar. -
- )} -
- )} -
- )} -
- {error &&
{error}
} {/* CTA */}