| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239 |
- import 'package:flutter/material.dart';
- import 'package:flutter_riverpod/flutter_riverpod.dart';
- import 'package:go_router/go_router.dart';
- import '../../../../data/models/analysis_models.dart';
- import '../../../../services/analysis_providers.dart';
- import '../../../shared/presentation/widgets/async_value_view.dart';
- import '../../../shared/presentation/widgets/lab_section_scaffold.dart';
- class ObservationPage extends ConsumerWidget {
- const ObservationPage({super.key});
- @override
- Widget build(BuildContext context, WidgetRef ref) {
- final snapshot = ref.watch(selectedSessionSnapshotProvider);
- final observation = ref.watch(selectedObservationProvider);
- return LabSectionScaffold(
- eyebrow: 'Observation',
- title: 'Inspect sample metadata and probe evidence.',
- description:
- 'This page will show the captured audio summary, waveform slices, probe output, and sample tags before deeper analysis begins.',
- children: [
- AsyncValueView(
- value: snapshot,
- loadingMessage: 'Loading observation snapshot...',
- data: (session) {
- if (session != null) {
- return _ObservationSessionView(session: session);
- }
- return AsyncValueView(
- value: observation,
- loadingMessage: 'Loading observation details...',
- data: (selectedObservation) {
- if (selectedObservation == null) {
- return const _EmptySelectionCard();
- }
- return _ObservationOnlyView(observation: selectedObservation);
- },
- );
- },
- ),
- ],
- );
- }
- }
- class _ObservationSessionView extends StatelessWidget {
- const _ObservationSessionView({required this.session});
- final AnalysisSessionSnapshot session;
- @override
- Widget build(BuildContext context) {
- return Column(
- children: [
- _ObservationDetailView(
- observation: session.observation,
- headerLine: 'Mode: ${session.mode} | Status: ${session.status}',
- ),
- const SizedBox(height: 16),
- Card(
- child: Padding(
- padding: const EdgeInsets.all(20),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- 'Probe Evidence',
- style: Theme.of(context).textTheme.titleLarge,
- ),
- const SizedBox(height: 12),
- ...session.probeEvidence.map(
- (evidence) => ListTile(
- contentPadding: EdgeInsets.zero,
- title: Text(
- '${evidence.category} | ${evidence.producerModuleId ?? 'unknown'}',
- ),
- subtitle: Text(
- evidence.values.entries
- .map((entry) => '${entry.key}: ${entry.value}')
- .join(' | '),
- ),
- trailing: Text(evidence.confidence.toStringAsFixed(2)),
- ),
- ),
- ],
- ),
- ),
- ),
- const SizedBox(height: 16),
- FilledButton(
- onPressed: () => context.goNamed('experiment'),
- child: const Text('Open Experiment Timeline'),
- ),
- ],
- );
- }
- }
- class _ObservationDetailView extends StatelessWidget {
- const _ObservationDetailView({
- required this.observation,
- this.headerLine,
- });
- final ObservationSummary observation;
- final String? headerLine;
- @override
- Widget build(BuildContext context) {
- final theme = Theme.of(context);
- return Card(
- child: Padding(
- padding: const EdgeInsets.all(20),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- 'Observation ${observation.id}',
- style: theme.textTheme.titleLarge,
- ),
- if (headerLine != null) ...[
- const SizedBox(height: 10),
- Text(headerLine!, style: theme.textTheme.bodyMedium),
- ],
- const SizedBox(height: 14),
- Text(
- 'Duration ${observation.durationMs} ms | ${observation.sampleRate} Hz | ${observation.channels} channel',
- style: theme.textTheme.bodyLarge,
- ),
- const SizedBox(height: 12),
- if (observation.tags.isEmpty)
- Text('No tags attached yet.', style: theme.textTheme.bodyMedium)
- else
- Wrap(
- spacing: 8,
- runSpacing: 8,
- children: observation.tags
- .map((tag) => Chip(label: Text(tag)))
- .toList(),
- ),
- if (observation.captureMetadata.isNotEmpty) ...[
- const SizedBox(height: 14),
- ...observation.captureMetadata.entries.map(
- (entry) => Padding(
- padding: const EdgeInsets.only(bottom: 4),
- child: Text('${entry.key}: ${entry.value}'),
- ),
- ),
- ],
- ],
- ),
- ),
- );
- }
- }
- class _ObservationOnlyView extends ConsumerStatefulWidget {
- const _ObservationOnlyView({required this.observation});
- final ObservationSummary observation;
- @override
- ConsumerState<_ObservationOnlyView> createState() =>
- _ObservationOnlyViewState();
- }
- class _ObservationOnlyViewState extends ConsumerState<_ObservationOnlyView> {
- bool _creatingSession = false;
- String? _errorMessage;
- @override
- Widget build(BuildContext context) {
- return Column(
- children: [
- _ObservationDetailView(observation: widget.observation),
- const SizedBox(height: 16),
- if (_errorMessage != null)
- Card(
- child: Padding(
- padding: const EdgeInsets.all(20),
- child: Text(_errorMessage!),
- ),
- ),
- if (_errorMessage != null) const SizedBox(height: 16),
- FilledButton(
- onPressed: _creatingSession ? null : _createSession,
- child: Text(
- _creatingSession ? 'Creating Analysis Session...' : 'Analyze This Observation',
- ),
- ),
- ],
- );
- }
- Future<void> _createSession() async {
- setState(() {
- _creatingSession = true;
- _errorMessage = null;
- });
- try {
- await ref
- .read(sessionActionsProvider)
- .createSessionForObservation(widget.observation.id);
- if (!mounted) return;
- context.goNamed('experiment');
- } catch (error) {
- if (!mounted) return;
- setState(() {
- _errorMessage = 'Failed to create analysis session: $error';
- });
- } finally {
- if (mounted) {
- setState(() {
- _creatingSession = false;
- });
- }
- }
- }
- }
- class _EmptySelectionCard extends StatelessWidget {
- const _EmptySelectionCard();
- @override
- Widget build(BuildContext context) {
- return const Card(
- child: Padding(
- padding: EdgeInsets.all(20),
- child: Text(
- 'No observation selected yet. Open one from History or create a new capture.',
- ),
- ),
- );
- }
- }
|