import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../../core/network/api_client.dart'; import '../../../core/theme/app_theme.dart'; import '../../../core/widgets/status_badge.dart'; import '../../asset_detail/views/asset_detail_screen.dart'; import '../../favorites/cubit/favorites_cubit.dart'; import '../models/news_article_model.dart'; import '../repositories/news_repository.dart'; import 'finbert_sentiment_tab.dart'; class ArticleSentimentDialog extends StatefulWidget { final NewsArticleModel articleData; final ApiClient? apiClient; final ValueChanged? onArticleUpdated; const ArticleSentimentDialog({ super.key, required this.articleData, this.apiClient, this.onArticleUpdated, }); @override State createState() => _ArticleSentimentDialogState(); } class _ArticleSentimentDialogState extends State with SingleTickerProviderStateMixin { late TabController _tabController; late NewsArticleModel _currentArticle; bool _isReanalyzing = false; String? _reanalyzeError; @override void initState() { super.initState(); _currentArticle = widget.articleData; _tabController = TabController(length: 2, vsync: this); } @override void dispose() { _tabController.dispose(); super.dispose(); } void _openOriginalSource() async { final urlStr = _currentArticle.sourceUrl; if (urlStr.isNotEmpty) { final uri = Uri.parse(urlStr); if (await canLaunchUrl(uri)) { await launchUrl(uri, mode: LaunchMode.externalApplication); } } } Future _reanalyzeSentiment() async { if (_isReanalyzing || _currentArticle.id.isEmpty) return; setState(() { _isReanalyzing = true; _reanalyzeError = null; }); try { final client = widget.apiClient ?? context.read(); final repo = NewsRepository(apiClient: client, backendUrl: ApiClient.baseUrl); final updatedArticle = await repo.reanalyzeArticle(_currentArticle.id); if (mounted) { setState(() { _currentArticle = updatedArticle; _isReanalyzing = false; }); widget.onArticleUpdated?.call(updatedArticle); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( children: [ Icon(Icons.check_circle, color: AppTheme.primaryEmerald, size: 20), const SizedBox(width: 8), const Text('Sentiment-Analyse erfolgreich erneuert!'), ], ), backgroundColor: AppTheme.cardSurface, duration: const Duration(seconds: 3), ), ); } } catch (e) { if (mounted) { setState(() { _isReanalyzing = false; _reanalyzeError = e.toString(); }); } } } @override Widget build(BuildContext context) { final article = _currentArticle; final title = article.title.isNotEmpty ? article.title : 'Nachrichtenartikel'; final author = article.author.isNotEmpty ? article.author : 'Finlytic News'; final summary = article.summary; final sourceUrl = article.sourceUrl; final contentRaw = article.contentRaw; final publishedAt = "${article.publishedAt.day.toString().padLeft(2, '0')}.${article.publishedAt.month.toString().padLeft(2, '0')}.${article.publishedAt.year}"; final status = article.status.isNotEmpty ? article.status : 'Completed'; final rawSentiment = article.sentiment; final compoundScore = article.finbertResult?.score ?? article.sentimentScore; final Widget listBadge = rawSentiment.isNotEmpty ? StatusBadge.sentiment(rawSentiment.toUpperCase(), score: compoundScore) : StatusBadge( label: status.toUpperCase(), color: status.toLowerCase().contains('analyz') ? AppTheme.primaryEmerald : AppTheme.accentCyan, ); final client = widget.apiClient ?? (context.mounted ? context.read() : null); final favourites = context.read().state.favoriteDetails; return Dialog( backgroundColor: AppTheme.cardSurface, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), side: BorderSide(color: AppTheme.glassBorder), ), child: Container( width: 720, height: 640, padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Colors.white), maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 6), Text( 'Quelle: $author • $publishedAt', style: TextStyle(color: AppTheme.textMuted, fontSize: 12), ), ], ), ), const SizedBox(width: 12), Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ // Sentiment Reanalyze Icon Button (directly left of badge) IconButton( tooltip: 'KI-Sentiment neu analysieren', onPressed: _isReanalyzing ? null : _reanalyzeSentiment, icon: _isReanalyzing ? SizedBox( width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2, color: AppTheme.accentCyan), ) : Icon(Icons.auto_awesome, size: 18, color: AppTheme.accentCyan), padding: const EdgeInsets.all(6), constraints: const BoxConstraints(), ), const SizedBox(width: 6), listBadge, const SizedBox(width: 8), IconButton( onPressed: () => Navigator.pop(context), icon: const Icon(Icons.close, color: Colors.white70, size: 20), padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), ], ), ], ), if (_reanalyzeError != null) ...[ const SizedBox(height: 8), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: AppTheme.accentRed.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(6), border: Border.all(color: AppTheme.accentRed.withValues(alpha: 0.4)), ), child: Row( children: [ Icon(Icons.error_outline, size: 16, color: AppTheme.accentRed), const SizedBox(width: 6), Expanded( child: Text( _reanalyzeError!, style: TextStyle(color: AppTheme.accentRed, fontSize: 11), ), ), ], ), ), ], const SizedBox(height: 12), // Tab Bar Container( decoration: BoxDecoration( color: AppTheme.glassSurface, borderRadius: BorderRadius.circular(10), border: Border.all(color: AppTheme.glassBorder), ), child: TabBar( controller: _tabController, indicatorColor: AppTheme.primaryEmerald, labelColor: AppTheme.primaryEmerald, unselectedLabelColor: AppTheme.textMuted, indicatorSize: TabBarIndicatorSize.tab, tabs: const [ Tab(icon: Icon(Icons.article_outlined, size: 18), text: 'Artikel & Assets'), Tab(icon: Icon(Icons.psychology_outlined, size: 18), text: 'FinBERT KI-Sentiment'), ], ), ), const SizedBox(height: 12), // Tab Views Expanded( child: TabBarView( controller: _tabController, children: [ SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (summary.isNotEmpty) ...[ const Text('Zusammenfassung:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Colors.white)), const SizedBox(height: 6), Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppTheme.glassSurface, borderRadius: BorderRadius.circular(8), border: Border.all(color: AppTheme.glassBorder), ), child: Text(summary, style: const TextStyle(color: Colors.white70, fontSize: 13, height: 1.4)), ), const SizedBox(height: 14), ], if (article.matchedAssets.isNotEmpty) ...[ Row( children: [ const Text('Erkannte Unternehmen & Assets:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Colors.white)), const SizedBox(width: 6), Text('(Klick öffnet Asset-Details)', style: TextStyle(color: AppTheme.textMuted, fontSize: 11)), ], ), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, children: article.matchedAssets.map((asset) { final isin = asset.isin; final name = asset.name.isNotEmpty ? asset.name : (asset.symbol.isNotEmpty ? asset.symbol : isin); final match = favourites.where((e) => e.isin == isin); final symbol = match.isEmpty ? null : match.first; return ActionChip( avatar: Icon(Icons.open_in_new, size: 14, color: AppTheme.accentCyan), label: Text( name, style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: AppTheme.accentCyan, ), ), backgroundColor: AppTheme.accentCyan.withValues(alpha: 0.1), side: BorderSide(color: AppTheme.accentCyan.withValues(alpha: 0.35)), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), onPressed: () { if (client != null && isin.isNotEmpty) { Navigator.push( context, MaterialPageRoute( builder: (_) => AssetDetailScreen( isin: isin, name: name, symbol: symbol != null ? symbol.symbol : asset.symbol, apiClient: client, ), ), ); } }, ); }).toList(), ), const SizedBox(height: 14), ], const Text('Vollständiger Artikeltext:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Colors.white)), const SizedBox(height: 6), Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(8), border: Border.all(color: AppTheme.glassBorder), ), child: Text( contentRaw.isNotEmpty ? contentRaw : (summary.isNotEmpty ? summary : 'Kein vollständiger Text verfügbar.'), style: const TextStyle(color: Colors.white70, fontSize: 13, height: 1.5), ), ), ], ), ), FinbertSentimentTab(article: article), ], ), ), const SizedBox(height: 12), // Footer Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ if (sourceUrl.isNotEmpty) TextButton.icon( onPressed: _openOriginalSource, icon: const Icon(Icons.open_in_new, size: 16), label: const Text('Originalquelle im Web öffnen'), style: TextButton.styleFrom(foregroundColor: AppTheme.accentCyan), ) else const SizedBox.shrink(), ElevatedButton( onPressed: () => Navigator.pop(context), style: ElevatedButton.styleFrom( backgroundColor: AppTheme.primaryEmerald, foregroundColor: Colors.black, padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), child: const Text('Schließen', style: TextStyle(fontWeight: FontWeight.bold)), ), ], ), ], ), ), ); } }