Files
diagd/lib/screens/dashboard_screen.dart

1079 lines
37 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:diagd/src/rust/api/diagnostics.dart';
import 'package:diagd/widgets/hardware_tab.dart';
import 'package:diagd/widgets/live_monitor_tab.dart';
import 'package:diagd/widgets/security_tab.dart';
import 'package:diagd/widgets/crashes_tab.dart';
class DashboardScreen extends StatefulWidget {
const DashboardScreen({super.key});
@override
State<DashboardScreen> createState() => _DashboardScreenState();
}
class _DashboardScreenState extends State<DashboardScreen> {
bool _isLoading = false;
bool _isExporting = false;
int _selectedTab = 0;
SystemReport? _report;
String? _errorMessage;
@override
void initState() {
super.initState();
_performScan();
}
Future<void> _performScan() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final report = await scanSystemAndAnalyze();
setState(() {
_report = report;
_isLoading = false;
});
} catch (e) {
setState(() {
_errorMessage = e.toString();
_isLoading = false;
});
}
}
Future<void> _exportDebugBundle() async {
if (_report == null) return;
setState(() {
_isExporting = true;
});
try {
final path = await exportDebugBundle(report: _report!);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.check_circle_rounded, color: Color(0xFF3FB950), size: 20),
const SizedBox(width: 10),
Expanded(
child: Text(
'Debug Bundle экспортирован: $path',
style: const TextStyle(fontSize: 13),
),
),
],
),
backgroundColor: const Color(0xFF161B22),
duration: const Duration(seconds: 5),
action: SnackBarAction(
label: 'Копировать путь',
textColor: const Color(0xFF58A6FF),
onPressed: () {
Clipboard.setData(ClipboardData(text: path));
},
),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Ошибка экспорта: $e'),
backgroundColor: const Color(0xFFF85149),
),
);
}
} finally {
if (mounted) {
setState(() {
_isExporting = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
appBar: _buildAppBar(),
body: _buildBody(),
);
}
PreferredSizeWidget _buildAppBar() {
return AppBar(
backgroundColor: const Color(0xFF161B22),
elevation: 0,
title: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: const Color(0xFF238636).withAlpha(40),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF238636).withAlpha(80)),
),
child: const Icon(
Icons.terminal_rounded,
color: Color(0xFF3FB950),
size: 22,
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'diagd • Linux AI Diagnostic Analyst',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: Color(0xFFE6EDF3),
letterSpacing: 0.2,
),
),
Text(
_report != null
? 'Последнее сканирование: ${_report!.timestamp}'
: 'Готов к диагностике системы',
style: const TextStyle(
fontSize: 12,
color: Color(0xFF8B949E),
),
),
],
),
],
),
actions: [
if (_report != null && _report!.promptPreview.isNotEmpty)
TextButton.icon(
onPressed: () => _showPromptModal(context, _report!.promptPreview),
icon: const Icon(Icons.psychology_outlined, size: 18, color: Color(0xFF58A6FF)),
label: const Text(
'LLM Prompt',
style: TextStyle(color: Color(0xFF58A6FF), fontSize: 13),
),
),
if (_report != null) ...[
const SizedBox(width: 4),
TextButton.icon(
onPressed: _isExporting ? null : _exportDebugBundle,
icon: _isExporting
? const SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF3FB950)),
)
: const Icon(Icons.archive_outlined, size: 18, color: Color(0xFF3FB950)),
label: Text(
_isExporting ? 'Экспорт...' : 'Экспорт отчета',
style: const TextStyle(color: Color(0xFF3FB950), fontSize: 13),
),
),
],
const SizedBox(width: 8),
Padding(
padding: const EdgeInsets.only(right: 16),
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF238636),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
onPressed: _isLoading ? null : _performScan,
icon: _isLoading
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(Icons.refresh_rounded, size: 18),
label: Text(
_isLoading ? 'Анализ...' : 'Начать сканирование',
style: const TextStyle(fontWeight: FontWeight.w600),
),
),
),
],
bottom: _report != null
? PreferredSize(
preferredSize: const Size.fromHeight(48),
child: Container(
decoration: const BoxDecoration(
color: Color(0xFF161B22),
border: Border(bottom: BorderSide(color: Color(0xFF30363D), width: 1)),
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
const SizedBox(width: 24),
_buildTabButton(
index: 0,
title: 'Диагностика',
badge: _report!.issues.isNotEmpty ? '${_report!.issues.length}' : null,
icon: Icons.troubleshoot_rounded,
),
const SizedBox(width: 6),
_buildTabButton(
index: 1,
title: 'Оборудование',
icon: Icons.memory_rounded,
),
const SizedBox(width: 6),
_buildTabButton(
index: 2,
title: 'Live Монитор',
icon: Icons.speed_rounded,
),
const SizedBox(width: 6),
_buildTabButton(
index: 3,
title: 'Безопасность',
badge: '${_report!.security.securityScore}%',
icon: Icons.security_rounded,
),
const SizedBox(width: 6),
_buildTabButton(
index: 4,
title: 'Аварии и Сбои',
badge: _report!.crashes.isNotEmpty ? '${_report!.crashes.length}' : null,
icon: Icons.bug_report_rounded,
),
const SizedBox(width: 24),
],
),
),
),
)
: null,
);
}
Widget _buildTabButton({
required int index,
required String title,
required IconData icon,
String? badge,
}) {
final isSelected = _selectedTab == index;
return InkWell(
onTap: () {
setState(() {
_selectedTab = index;
});
},
borderRadius: const BorderRadius.vertical(top: Radius.circular(8)),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: isSelected ? const Color(0xFFF78166) : Colors.transparent,
width: 2,
),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 16,
color: isSelected ? const Color(0xFFE6EDF3) : const Color(0xFF8B949E),
),
const SizedBox(width: 8),
Text(
title,
style: TextStyle(
fontSize: 13,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
color: isSelected ? const Color(0xFFE6EDF3) : const Color(0xFF8B949E),
),
),
if (badge != null) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
decoration: BoxDecoration(
color: const Color(0xFF30363D),
borderRadius: BorderRadius.circular(10),
),
child: Text(
badge,
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Color(0xFFE6EDF3),
),
),
),
],
],
),
),
);
}
Widget _buildBody() {
if (_isLoading && _report == null) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(color: Color(0xFF58A6FF)),
SizedBox(height: 20),
Text(
'Опрос systemd, чтение journalctl / dmesg и санитизация логов...',
style: TextStyle(color: Color(0xFF8B949E), fontSize: 14),
),
],
),
);
}
if (_errorMessage != null && _report == null) {
return Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 600),
margin: const EdgeInsets.all(24),
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: const Color(0xFF21262D),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFF85149).withAlpha(100)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline_rounded, color: Color(0xFFF85149), size: 48),
const SizedBox(height: 16),
const Text(
'Ошибка сканирования системы',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white),
),
const SizedBox(height: 8),
Text(
_errorMessage!,
textAlign: TextAlign.center,
style: const TextStyle(color: Color(0xFF8B949E), fontFamily: 'monospace'),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _performScan,
child: const Text('Повторить'),
),
],
),
),
);
}
if (_report == null) {
return const SizedBox.shrink();
}
if (_selectedTab == 1) {
return HardwareTab(hardware: _report!.hardware);
} else if (_selectedTab == 2) {
return const LiveMonitorTab();
} else if (_selectedTab == 3) {
return SecurityTab(security: _report!.security);
} else if (_selectedTab == 4) {
return CrashesTab(crashes: _report!.crashes);
}
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSummaryBanner(_report!),
const SizedBox(height: 24),
const Text(
'СОСТОЯНИЕ ПОДСИСТЕМ',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
letterSpacing: 1.2,
color: Color(0xFF8B949E),
),
),
const SizedBox(height: 12),
_buildSubsystemsGrid(_report!.subsystems),
const SizedBox(height: 28),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'НАЙДЕННЫЕ ИНЦИДЕНТЫ (${_report!.issues.length})',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
letterSpacing: 1.2,
color: Color(0xFF8B949E),
),
),
if (_report!.llmAnalyzed)
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFF1F6FEB).withAlpha(40),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF58A6FF)),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.auto_awesome, size: 14, color: Color(0xFF58A6FF)),
SizedBox(width: 6),
Text(
'Обработано LLM',
style: TextStyle(fontSize: 11, color: Color(0xFF58A6FF), fontWeight: FontWeight.w600),
),
],
),
),
],
),
const SizedBox(height: 12),
_buildIssuesList(_report!.issues),
],
),
);
}
Widget _buildSummaryBanner(SystemReport report) {
final criticalCount = report.issues.where((i) => i.severity == Severity.critical).length;
final warningCount = report.issues.where((i) => i.severity == Severity.warning).length;
final Color statusColor = criticalCount > 0
? const Color(0xFFF85149)
: (warningCount > 0 ? const Color(0xFFD29922) : const Color(0xFF3FB950));
return Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: statusColor.withAlpha(80)),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: statusColor.withAlpha(30),
shape: BoxShape.circle,
),
child: Icon(
criticalCount > 0
? Icons.warning_amber_rounded
: (warningCount > 0 ? Icons.info_outline_rounded : Icons.check_circle_outline_rounded),
color: statusColor,
size: 28,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
criticalCount > 0
? 'Обнаружены критические сбои системы'
: (warningCount > 0 ? 'Обнаружены предупреждения' : 'Система полностью стабильна'),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFFE6EDF3),
),
),
const SizedBox(height: 4),
Text(
report.summary,
style: const TextStyle(fontSize: 13, color: Color(0xFF8B949E)),
),
],
),
),
Row(
children: [
_buildStatChip('Критические', criticalCount, const Color(0xFFF85149)),
const SizedBox(width: 8),
_buildStatChip('Предупреждения', warningCount, const Color(0xFFD29922)),
],
),
],
),
);
}
Widget _buildStatChip(String label, int count, Color color) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: color.withAlpha(25),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withAlpha(70)),
),
child: Column(
children: [
Text(
count.toString(),
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: color,
),
),
Text(
label,
style: const TextStyle(fontSize: 10, color: Color(0xFF8B949E)),
),
],
),
);
}
Widget _buildSubsystemsGrid(List<SubsystemStatus> subsystems) {
return LayoutBuilder(
builder: (context, constraints) {
final crossAxisCount = constraints.maxWidth > 900 ? 5 : (constraints.maxWidth > 550 ? 3 : 2);
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: subsystems.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
mainAxisExtent: 110,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemBuilder: (context, index) {
final sub = subsystems[index];
return _buildSubsystemCard(sub);
},
);
},
);
}
Widget _buildSubsystemCard(SubsystemStatus sub) {
final (IconData icon, String name) = switch (sub.name) {
Subsystem.systemd => (Icons.settings_suggest_rounded, 'Systemd'),
Subsystem.kernel => (Icons.memory_rounded, 'Kernel'),
Subsystem.storage => (Icons.storage_rounded, 'Storage'),
Subsystem.audio => (Icons.volume_up_rounded, 'Audio'),
Subsystem.network => (Icons.lan_rounded, 'Network'),
Subsystem.security => (Icons.security_rounded, 'Security'),
Subsystem.crashes => (Icons.bug_report_rounded, 'Crashes'),
};
final isHealthy = sub.status == 'Healthy';
final isFailed = sub.status == 'Failed';
final color = isFailed
? const Color(0xFFF85149)
: (isHealthy ? const Color(0xFF3FB950) : const Color(0xFFD29922));
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: color.withAlpha(60)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(icon, color: const Color(0xFFE6EDF3), size: 20),
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(color: color.withAlpha(120), blurRadius: 4, spreadRadius: 1),
],
),
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Color(0xFFE6EDF3),
),
),
const SizedBox(height: 2),
Text(
sub.status,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: color,
),
),
],
),
],
),
);
}
Widget _buildIssuesList(List<IssueItem> issues) {
if (issues.isEmpty) {
return Container(
padding: const EdgeInsets.all(32),
width: double.infinity,
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(12),
),
child: const Center(
child: Text(
'Проблем не обнаружено',
style: TextStyle(color: Color(0xFF8B949E)),
),
),
);
}
return ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: issues.length,
separatorBuilder: (_, _) => const SizedBox(height: 16),
itemBuilder: (context, index) {
return _buildIssueCard(issues[index]);
},
);
}
Widget _buildIssueCard(IssueItem issue) {
final (badgeColor, badgeText, badgeIcon) = switch (issue.severity) {
Severity.critical => (
const Color(0xFFF85149),
'CRITICAL',
Icons.error_outline_rounded,
),
Severity.warning => (
const Color(0xFFD29922),
'WARNING',
Icons.warning_amber_rounded,
),
Severity.info => (
const Color(0xFF58A6FF),
'INFO',
Icons.info_outline_rounded,
),
};
return Container(
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF30363D)),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Верхняя плашка: Бейдж критичности + Подсистема
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: badgeColor.withAlpha(35),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: badgeColor.withAlpha(90)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(badgeIcon, size: 13, color: badgeColor),
const SizedBox(width: 4),
Text(
badgeText,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: badgeColor,
letterSpacing: 0.5,
),
),
],
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: const Color(0xFF21262D),
borderRadius: BorderRadius.circular(6),
),
child: Text(
issue.subsystem.name.toUpperCase(),
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Color(0xFF8B949E),
),
),
),
const Spacer(),
if (issue.sanitized)
const Tooltip(
message: 'Логи очищены от персональных путей, IP и токенов',
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.shield_outlined, size: 14, color: Color(0xFF3FB950)),
SizedBox(width: 4),
Text(
'Sanitized',
style: TextStyle(fontSize: 11, color: Color(0xFF3FB950)),
),
],
),
),
],
),
const SizedBox(height: 12),
// Заголовок проблемы
Text(
issue.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: Color(0xFFE6EDF3),
),
),
const SizedBox(height: 8),
// Корень проблемы (Root Cause)
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF0D1117),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF21262D)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'КОРЕНЬ ПРОБЛЕМЫ (ROOT CAUSE)',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w700,
letterSpacing: 0.8,
color: Color(0xFF58A6FF),
),
),
const SizedBox(height: 4),
Text(
issue.rootCause,
style: const TextStyle(
fontSize: 13,
color: Color(0xFFC9D1D9),
height: 1.4,
),
),
const SizedBox(height: 8),
Text(
issue.explanation,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF8B949E),
height: 1.3,
),
),
],
),
),
// Блок с решением / командой
if (issue.fix != null) ...[
const SizedBox(height: 14),
_buildFixCommandSection(issue.fix!),
],
const SizedBox(height: 14),
// Кнопка показа сырого лога
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed: () => _showRawLogModal(context, issue.title, issue.rawLog),
icon: const Icon(Icons.code_rounded, size: 16, color: Color(0xFF8B949E)),
label: const Text(
'Показать сырой лог',
style: TextStyle(fontSize: 12, color: Color(0xFF8B949E)),
),
),
),
],
),
),
);
}
Widget _buildFixCommandSection(FixSuggestion fix) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF0D1117),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF388BFD).withAlpha(50)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.build_circle_outlined, size: 15, color: Color(0xFF3FB950)),
const SizedBox(width: 6),
Text(
fix.title,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF3FB950),
),
),
],
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: const Color(0xFF30363D)),
),
child: Row(
children: [
Expanded(
child: SelectableText(
fix.command,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
color: Color(0xFF79C0FF),
),
),
),
IconButton(
tooltip: 'Скопировать команду решения',
icon: const Icon(Icons.copy_rounded, size: 16, color: Color(0xFF58A6FF)),
onPressed: () {
Clipboard.setData(ClipboardData(text: fix.command));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Команда скопирована: ${fix.command}'),
duration: const Duration(seconds: 2),
backgroundColor: const Color(0xFF238636),
),
);
},
),
],
),
),
if (fix.explanation.isNotEmpty) ...[
const SizedBox(height: 6),
Text(
fix.explanation,
style: const TextStyle(fontSize: 11, color: Color(0xFF8B949E)),
),
],
],
),
);
}
void _showRawLogModal(BuildContext context, String title, String rawLog) {
showDialog(
context: context,
builder: (context) {
return Dialog(
backgroundColor: const Color(0xFF161B22),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: const BorderSide(color: Color(0xFF30363D)),
),
child: Container(
constraints: const BoxConstraints(maxWidth: 750, maxHeight: 600),
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Системный лог: $title',
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Color(0xFFE6EDF3),
),
),
),
IconButton(
icon: const Icon(Icons.close_rounded, color: Color(0xFF8B949E)),
onPressed: () => Navigator.pop(context),
),
],
),
const Divider(color: Color(0xFF30363D)),
const SizedBox(height: 8),
Expanded(
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF0D1117),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF21262D)),
),
child: SingleChildScrollView(
child: SelectableText(
rawLog.isEmpty ? 'Лог пуст или нет дополнительных записей' : rawLog,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
color: Color(0xFFC9D1D9),
height: 1.4,
),
),
),
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton.icon(
onPressed: () {
Clipboard.setData(ClipboardData(text: rawLog));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Лог скопирован в буфер обмена'),
duration: Duration(seconds: 2),
),
);
},
icon: const Icon(Icons.copy_rounded, size: 16),
label: const Text('Скопировать весь лог'),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: const Text('Закрыть'),
),
],
),
],
),
),
);
},
);
}
void _showPromptModal(BuildContext context, String promptPreview) {
showDialog(
context: context,
builder: (context) {
return Dialog(
backgroundColor: const Color(0xFF161B22),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: const BorderSide(color: Color(0xFF30363D)),
),
child: Container(
constraints: const BoxConstraints(maxWidth: 700, maxHeight: 600),
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Структурированный JSON-промпт для LLM',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Color(0xFFE6EDF3),
),
),
IconButton(
icon: const Icon(Icons.close_rounded, color: Color(0xFF8B949E)),
onPressed: () => Navigator.pop(context),
),
],
),
const Divider(color: Color(0xFF30363D)),
const SizedBox(height: 8),
Expanded(
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF0D1117),
borderRadius: BorderRadius.circular(8),
),
child: SingleChildScrollView(
child: SelectableText(
promptPreview,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 11,
color: Color(0xFF79C0FF),
),
),
),
),
),
const SizedBox(height: 12),
Align(
alignment: Alignment.centerRight,
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
child: const Text('Закрыть'),
),
),
],
),
),
);
},
);
}
}