experiment_page.dart 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import 'package:go_router/go_router.dart';
  4. import '../../../../services/analysis_providers.dart';
  5. import '../../../shared/presentation/widgets/async_value_view.dart';
  6. import '../../../shared/presentation/widgets/lab_section_scaffold.dart';
  7. class ExperimentPage extends ConsumerWidget {
  8. const ExperimentPage({super.key});
  9. @override
  10. Widget build(BuildContext context, WidgetRef ref) {
  11. final theme = Theme.of(context);
  12. final snapshot = ref.watch(selectedSessionSnapshotProvider);
  13. return LabSectionScaffold(
  14. eyebrow: 'Experiment',
  15. title: 'Follow the AI analyst through each validation step.',
  16. description:
  17. 'This page visualizes hypotheses, validation chains, score changes, and structured failure reasons across the experiment timeline.',
  18. children: [
  19. AsyncValueView(
  20. value: snapshot,
  21. loadingMessage: 'Loading experiment timeline...',
  22. data: (session) {
  23. if (session == null) {
  24. return const _EmptySessionCard();
  25. }
  26. return Column(
  27. children: [
  28. Card(
  29. child: Padding(
  30. padding: const EdgeInsets.all(20),
  31. child: Column(
  32. crossAxisAlignment: CrossAxisAlignment.start,
  33. children: [
  34. Text('Hypotheses', style: theme.textTheme.titleLarge),
  35. const SizedBox(height: 12),
  36. ...session.hypotheses.map(
  37. (hypothesis) => Padding(
  38. padding: const EdgeInsets.only(bottom: 14),
  39. child: Column(
  40. crossAxisAlignment: CrossAxisAlignment.start,
  41. children: [
  42. Text(
  43. '${hypothesis.label} | ${hypothesis.confidence.toStringAsFixed(2)}',
  44. style: theme.textTheme.titleMedium,
  45. ),
  46. const SizedBox(height: 4),
  47. Text(
  48. hypothesis.rationale,
  49. style: theme.textTheme.bodyMedium,
  50. ),
  51. const SizedBox(height: 6),
  52. Wrap(
  53. spacing: 8,
  54. runSpacing: 8,
  55. children: hypothesis.relatedSignalProfiles
  56. .map((item) => Chip(label: Text(item)))
  57. .toList(),
  58. ),
  59. ],
  60. ),
  61. ),
  62. ),
  63. ],
  64. ),
  65. ),
  66. ),
  67. const SizedBox(height: 16),
  68. ...session.experiments.map(
  69. (experiment) => Card(
  70. child: Padding(
  71. padding: const EdgeInsets.all(20),
  72. child: Column(
  73. crossAxisAlignment: CrossAxisAlignment.start,
  74. children: [
  75. Text(
  76. experiment.title,
  77. style: theme.textTheme.titleLarge,
  78. ),
  79. const SizedBox(height: 8),
  80. Text(
  81. 'Status: ${experiment.status} | Score: ${experiment.score.total.toStringAsFixed(2)}',
  82. style: theme.textTheme.bodyMedium,
  83. ),
  84. if (experiment.hypothesisId != null) ...[
  85. const SizedBox(height: 6),
  86. Text(
  87. 'Hypothesis: ${experiment.hypothesisId}',
  88. style: theme.textTheme.bodyMedium,
  89. ),
  90. ],
  91. const SizedBox(height: 12),
  92. ...experiment.pipelineSummary.map(
  93. (node) => Padding(
  94. padding: const EdgeInsets.only(bottom: 4),
  95. child: Text('- $node'),
  96. ),
  97. ),
  98. if (experiment.score.notes.isNotEmpty) ...[
  99. const SizedBox(height: 12),
  100. ...experiment.score.notes.map(
  101. (note) => Text('Note: $note'),
  102. ),
  103. ],
  104. if (experiment.failureReasons.isNotEmpty) ...[
  105. const SizedBox(height: 12),
  106. Text(
  107. 'Failure reasons: ${experiment.failureReasons.join(', ')}',
  108. style: theme.textTheme.bodyMedium,
  109. ),
  110. ],
  111. ],
  112. ),
  113. ),
  114. ),
  115. ),
  116. const SizedBox(height: 8),
  117. FilledButton(
  118. onPressed: () => context.goNamed('conclusion'),
  119. child: const Text('Review Conclusion'),
  120. ),
  121. ],
  122. );
  123. },
  124. ),
  125. ],
  126. );
  127. }
  128. }
  129. class _EmptySessionCard extends StatelessWidget {
  130. const _EmptySessionCard();
  131. @override
  132. Widget build(BuildContext context) {
  133. return const Card(
  134. child: Padding(
  135. padding: EdgeInsets.all(20),
  136. child: Text('No session selected yet.'),
  137. ),
  138. );
  139. }
  140. }