import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; import '../models/news_article_model.dart'; import '../../../core/theme/app_theme.dart'; import '../../../core/widgets/status_badge.dart'; /// Two-Tab Dialog for Article details and real FinBERT Sentiment Analysis. class ArticleSentimentDialog extends StatefulWidget { final NewsArticleModel articleData; const ArticleSentimentDialog({ super.key, required this.articleData, }); @override State createState() => _ArticleSentimentDialogState(); } class _ArticleSentimentDialogState extends State with SingleTickerProviderStateMixin { late TabController _tabController; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); } @override void dispose() { _tabController.dispose(); super.dispose(); } String? _findString(List keys) { final artMap = widget.articleData.toJson(); for (final k in keys) { if (artMap.containsKey(k) && artMap[k] != null) { final val = artMap[k].toString().trim(); if (val.isNotEmpty) return val; } } return null; } void _openOriginalSource() async { final urlStr = widget.articleData.sourceUrl; if (urlStr.isNotEmpty) { final uri = Uri.parse(urlStr); if (await canLaunchUrl(uri)) { await launchUrl(uri, mode: LaunchMode.externalApplication); } } } Map? get _realSentimentData { if (widget.articleData.finbertResult != null) { return {'finbert_result': widget.articleData.finbertResult!.toJson()}; } return null; } bool get _isLoadingSentiment => false; @override Widget build(BuildContext context) { final title = _findString(['Title', 'title']) ?? 'Nachrichtenartikel'; final author = _findString(['Author', 'author', 'Source', 'source']) ?? 'Finlytic News'; final summary = _findString(['Summary', 'summary']); final sourceUrl = _findString(['SourceUrl', 'sourceUrl']); final contentRaw = _findString(['ContentRaw', 'contentRaw', 'content', 'Text', 'text']); final publishedAt = _findString(['PublishedAt', 'publishedAt', 'ScrapedAt', 'scrapedAt']) ?? ''; final status = _findString(['Status', 'status']) ?? 'Completed'; final rawSentiment = _findString(['sentiment', 'Sentiment', 'sentimentLabel', 'SentimentLabel']); final articleMap = widget.articleData.toJson(); final Map articleObj = articleMap.containsKey('article') && articleMap['article'] is Map ? Map.from(articleMap['article']) : articleMap; final matchedAssetsRaw = articleObj['MatchedAssets'] ?? articleObj['matchedAssets'] ?? articleMap['MatchedAssets'] ?? articleMap['matchedAssets']; final matchedAssets = matchedAssetsRaw is List ? matchedAssetsRaw : []; final double compoundScore = widget.articleData.finbertResult?.score ?? widget.articleData.sentimentScore; final double confidenceScore = widget.articleData.confidence; final String label = (widget.articleData.finbertResult?.label ?? widget.articleData.sentiment).toString().toUpperCase(); final double posRatio = widget.articleData.finbertResult?.positiveProbability ?? 0.0; final double neuRatio = widget.articleData.finbertResult?.neutralProbability ?? 0.0; final double negRatio = widget.articleData.finbertResult?.negativeProbability ?? 0.0; final String aiText = widget.articleData.finbertResult?.summarySnippet ?? summary ?? 'FinBERT Sentiment-Analyse verarbeitet.'; final Widget listBadge = rawSentiment != null ? StatusBadge.sentiment(rawSentiment.toUpperCase(), score: compoundScore) : StatusBadge( label: status.toUpperCase(), color: status.toLowerCase().contains('analyz') || status.toLowerCase().contains('klassifi') ? AppTheme.primaryEmerald : AppTheme.accentCyan, ); return Dialog( insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 30), child: Container( width: 700, height: 620, padding: const EdgeInsets.all(20), child: Column( children: [ TabBar( controller: _tabController, indicatorColor: AppTheme.primaryEmerald, labelColor: AppTheme.primaryEmerald, unselectedLabelColor: AppTheme.textMuted, tabs: const [ Tab(icon: Icon(Icons.article_outlined), text: 'Artikel & Volltext'), Tab(icon: Icon(Icons.psychology_outlined), text: 'Sentiment-Analyse (FinBERT)'), ], ), const SizedBox(height: 14), Expanded( child: TabBarView( controller: _tabController, children: [ // Tab 1: Artikel & Volltext SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, height: 1.3)), ), if (sourceUrl != null && sourceUrl.isNotEmpty) IconButton( icon: Icon(Icons.open_in_new, color: AppTheme.primaryEmerald, size: 22), tooltip: 'Originalquelle lesen', onPressed: _openOriginalSource, ), ], ), const SizedBox(height: 8), Row( children: [ Expanded( child: Text( '$author ${publishedAt.isNotEmpty ? "• $publishedAt" : ""}', style: TextStyle(color: AppTheme.textMuted, fontSize: 12), ), ), listBadge, ], ), if (matchedAssets.isNotEmpty) ...[ const SizedBox(height: 12), Text('Verknüpfte Wertpapiere (Matched Assets):', style: TextStyle(color: AppTheme.textMuted, fontSize: 11, fontWeight: FontWeight.bold)), const SizedBox(height: 4), Wrap( spacing: 6, runSpacing: 6, children: matchedAssets.map((a) { final name = a['Name'] ?? a['name'] ?? a['Isin'] ?? a['isin'] ?? 'Asset'; final isin = a['Isin'] ?? a['isin'] ?? ''; return Chip( label: Text( isin.toString().isNotEmpty && name.toString() != isin.toString() ? '${name.toString()} ($isin)' : name.toString(), style: TextStyle(fontSize: 11, color: AppTheme.accentCyan), ), backgroundColor: AppTheme.glassSurface, side: BorderSide(color: AppTheme.glassBorder), padding: EdgeInsets.zero, ); }).toList(), ), ], Divider(height: 24, color: AppTheme.glassBorder), if (summary != null && summary.isNotEmpty) ...[ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppTheme.glassSurface, borderRadius: BorderRadius.circular(8), border: Border.all(color: AppTheme.glassBorder), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Zusammenfassung:', style: TextStyle(fontWeight: FontWeight.bold, color: AppTheme.accentCyan, fontSize: 12)), const SizedBox(height: 4), Text(summary, style: TextStyle(fontSize: 13, height: 1.4, color: AppTheme.textPrimary)), ], ), ), const SizedBox(height: 16), ], const Text('Vollständiger Artikeltext:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14)), const SizedBox(height: 8), Text( contentRaw ?? summary ?? 'Kein Volltext verfügbar.', style: TextStyle(fontSize: 13.5, height: 1.6, color: AppTheme.textSecondary), ), ], ), ), // Tab 2: Real Sentiment Analysis (FinBERT) SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('FinBERT KI Klassifizierung', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), if (_realSentimentData != null) StatusBadge.sentiment(label, score: compoundScore), ], ), const SizedBox(height: 16), if (_isLoadingSentiment) Padding( padding: const EdgeInsets.symmetric(vertical: 40), child: Center( child: CircularProgressIndicator(color: AppTheme.primaryEmerald, strokeWidth: 2), ), ) else if (_realSentimentData == null) Container( width: double.infinity, margin: const EdgeInsets.only(top: 20), padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: AppTheme.glassSurface, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppTheme.glassBorder), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.info_outline, color: AppTheme.accentCyan, size: 44), const SizedBox(height: 12), const Text( 'Keine Sentiment-Analyse vorhanden', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white), textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( 'Für diesen Artikel liegt aktuell noch keine FinBERT-Sentiment-Analyse vor.', style: TextStyle(fontSize: 13, color: AppTheme.textMuted, height: 1.4), textAlign: TextAlign.center, ), ], ), ) else ...[ Row( children: [ SentimentMetricCard( title: 'Compound Score', valueText: '${compoundScore > 0 ? "+" : ""}${compoundScore.toStringAsFixed(2)}', tooltipText: 'Der Compound-Score misst die aggregierte Gesamtausrichtung der Nachricht auf einer Skala von -1.00 (sehr negativ) bis +1.00 (sehr positiv). Werte ab +0.15 gelten als positiv.', accentColor: compoundScore > 0.15 ? AppTheme.primaryEmerald : (compoundScore < -0.15 ? AppTheme.accentRed : AppTheme.accentCyan), ), const SizedBox(width: 12), SentimentMetricCard( title: 'KI-Confidence', valueText: '${(confidenceScore * 100).toInt()}%', tooltipText: 'Die Confidence gibt die statistische Wahrscheinlichkeit (0% bis 100%) an, mit welcher die FinBERT KI ihre Stimmungszuordnung berechnet hat.', accentColor: AppTheme.accentCyan, progressValue: confidenceScore, ), ], ), const SizedBox(height: 16), Text('Echte FinBERT Softmax-Verteilung:', style: TextStyle(color: AppTheme.textMuted, fontSize: 12, fontWeight: FontWeight.bold)), const SizedBox(height: 8), ArticleSentimentMeterBar(label: 'Positiv', ratio: posRatio, color: AppTheme.primaryEmerald), const SizedBox(height: 6), ArticleSentimentMeterBar(label: 'Neutral', ratio: neuRatio, color: AppTheme.accentCyan), const SizedBox(height: 6), ArticleSentimentMeterBar(label: 'Negativ', ratio: negRatio, color: AppTheme.accentRed), Divider(height: 24, color: AppTheme.glassBorder), const Text('KI-Zusammenfassung & Auswirkung:', style: TextStyle(fontWeight: FontWeight.bold)), const SizedBox(height: 6), Text( aiText, style: TextStyle(fontSize: 13, height: 1.4, color: AppTheme.textSecondary), ), ], ], ), ), ], ), ), ], ), ), ); } } /// Extracted metric card component with interactive info explanation tooltips. class SentimentMetricCard extends StatelessWidget { final String title; final String valueText; final String tooltipText; final Color accentColor; final double? progressValue; const SentimentMetricCard({ super.key, required this.title, required this.valueText, required this.tooltipText, required this.accentColor, this.progressValue, }); @override Widget build(BuildContext context) { return Expanded( child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppTheme.glassSurface, borderRadius: BorderRadius.circular(10), border: Border.all(color: AppTheme.glassBorder), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( title, style: TextStyle(fontSize: 12, color: AppTheme.textMuted, fontWeight: FontWeight.bold), overflow: TextOverflow.ellipsis, ), ), Tooltip( message: tooltipText, padding: const EdgeInsets.all(12), margin: const EdgeInsets.symmetric(horizontal: 24), decoration: BoxDecoration( color: const Color(0xFF1E2130), borderRadius: BorderRadius.circular(8), border: Border.all(color: AppTheme.glassBorder), ), textStyle: const TextStyle(color: Colors.white, fontSize: 12, height: 1.3), child: Icon(Icons.info_outline, size: 16, color: AppTheme.accentCyan), ), ], ), const SizedBox(height: 6), Text( valueText, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: accentColor), ), if (progressValue != null) ...[ const SizedBox(height: 6), LinearProgressIndicator( value: progressValue!.clamp(0.0, 1.0), backgroundColor: AppTheme.glassBorder, color: accentColor, minHeight: 6, ), ], ], ), ), ); } } /// Extracted sub-component for displaying sentiment probability bars. class ArticleSentimentMeterBar extends StatelessWidget { final String label; final double ratio; final Color color; const ArticleSentimentMeterBar({ super.key, required this.label, required this.ratio, required this.color, }); @override Widget build(BuildContext context) { final clampedRatio = ratio.clamp(0.0, 1.0); return Row( children: [ SizedBox(width: 60, child: Text(label, style: const TextStyle(fontSize: 12))), Expanded( child: LinearProgressIndicator( value: clampedRatio, backgroundColor: AppTheme.glassSurface, color: color, minHeight: 10, ), ), const SizedBox(width: 10), Text('${(clampedRatio * 100).toInt()}%', style: TextStyle(color: color, fontWeight: FontWeight.bold, fontSize: 12)), ], ); } }