diff --git a/apps/mobile/apps/client/lib/main.dart b/apps/mobile/apps/client/lib/main.dart index 2917828c..301ade92 100644 --- a/apps/mobile/apps/client/lib/main.dart +++ b/apps/mobile/apps/client/lib/main.dart @@ -51,7 +51,10 @@ void main() async { /// The main application module for the Client app. class AppModule extends Module { @override - List get imports => [core_localization.LocalizationModule()]; + List get imports => [ + core_localization.LocalizationModule(), + CoreModule(), + ]; @override void routes(RouteManager r) { diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json index 82b9cebe..2ecac88b 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json @@ -349,6 +349,7 @@ "listening": "Listening...", "send": "Send Message", "sending": "Sending...", + "transcribing": "Transcribing...", "success_title": "Request Sent!", "success_message": "We're finding available workers for you right now. You'll be notified as they accept.", "back_to_orders": "Back to Orders" diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json index dd5a0c73..00d8d979 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json @@ -349,6 +349,7 @@ "listening": "Escuchando...", "send": "Enviar Mensaje", "sending": "Enviando...", + "transcribing": "Transcribiendo...", "success_title": "\u00a1Solicitud Enviada!", "success_message": "Estamos encontrando trabajadores disponibles para ti ahora mismo. Te notificaremos cuando acepten.", "back_to_orders": "Volver a \u00d3rdenes" diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/create_order_module.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/create_order_module.dart index db8d49ed..98f02894 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/create_order_module.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/create_order_module.dart @@ -26,13 +26,13 @@ import 'presentation/pages/recurring_order_page.dart'; /// presentation layer BLoCs. class ClientCreateOrderModule extends Module { @override - List get imports => [DataConnectModule()]; + List get imports => [DataConnectModule(), CoreModule()]; @override void binds(Injector i) { // Repositories i.addLazySingleton( - (Injector i) => ClientCreateOrderRepositoryImpl( + () => ClientCreateOrderRepositoryImpl( service: i.get(), rapidOrderService: i.get(), ), @@ -49,7 +49,7 @@ class ClientCreateOrderModule extends Module { // BLoCs i.add( - (Injector i) => RapidOrderBloc( + () => RapidOrderBloc( i.get(), i.get(), i.get(), diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/rapid_order/rapid_order_bloc.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/rapid_order/rapid_order_bloc.dart index 3b957100..f9cc14e6 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/rapid_order/rapid_order_bloc.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/rapid_order/rapid_order_bloc.dart @@ -15,7 +15,7 @@ class RapidOrderBloc extends Bloc this._parseRapidOrderUseCase, this._audioRecorderService, ) : super( - const RapidOrderInitial( + const RapidOrderState( examples: [ '"We had a call out. Need 2 cooks ASAP"', '"Need 5 bartenders ASAP until 5am"', @@ -36,33 +36,76 @@ class RapidOrderBloc extends Bloc RapidOrderMessageChanged event, Emitter emit, ) { - if (state is RapidOrderInitial) { - emit((state as RapidOrderInitial).copyWith(message: event.message)); - } + emit( + state.copyWith(message: event.message, status: RapidOrderStatus.initial), + ); } Future _onVoiceToggled( RapidOrderVoiceToggled event, Emitter emit, ) async { - if (state is RapidOrderInitial) { - final RapidOrderInitial currentState = state as RapidOrderInitial; - final bool newListeningState = !currentState.isListening; - - emit(currentState.copyWith(isListening: newListeningState)); - - // Simulate voice recognition - if (newListeningState) { - await Future.delayed(const Duration(seconds: 2)); - if (state is RapidOrderInitial) { + if (!state.isListening) { + // Start Recording + await handleError( + emit: emit.call, + action: () async { + await _audioRecorderService.startRecording(); emit( - (state as RapidOrderInitial).copyWith( - message: 'Need 2 servers for a banquet right now.', + state.copyWith(isListening: true, status: RapidOrderStatus.initial), + ); + }, + onError: (String errorKey) => + state.copyWith(status: RapidOrderStatus.failure, error: errorKey), + ); + } else { + // Stop Recording and Transcribe + await handleError( + emit: emit.call, + action: () async { + // 1. Stop recording + final String? audioPath = await _audioRecorderService.stopRecording(); + + if (audioPath == null) { + emit( + state.copyWith( + isListening: false, + status: RapidOrderStatus.initial, + ), + ); + return; + } + + // 2. Transcribe + emit( + state.copyWith( isListening: false, + isTranscribing: true, + status: RapidOrderStatus.initial, ), ); - } - } + + final String transcription = await _transcribeRapidOrderUseCase( + audioPath, + ); + + // 3. Update message + emit( + state.copyWith( + message: transcription, + isListening: false, + isTranscribing: false, + status: RapidOrderStatus.initial, + ), + ); + }, + onError: (String errorKey) => state.copyWith( + status: RapidOrderStatus.failure, + error: errorKey, + isListening: false, + isTranscribing: false, + ), + ); } } @@ -70,30 +113,29 @@ class RapidOrderBloc extends Bloc RapidOrderSubmitted event, Emitter emit, ) async { - final RapidOrderState currentState = state; - if (currentState is RapidOrderInitial) { - final String message = currentState.message; - emit(const RapidOrderSubmitting()); + final String message = state.message; + emit(state.copyWith(status: RapidOrderStatus.submitting)); - await handleError( - emit: emit.call, - action: () async { - final OneTimeOrder order = await _parseRapidOrderUseCase(message); - emit(RapidOrderParsed(order)); - }, - onError: (String errorKey) => RapidOrderFailure(errorKey), - ); - } + await handleError( + emit: emit.call, + action: () async { + final OneTimeOrder order = await _parseRapidOrderUseCase(message); + emit( + state.copyWith(status: RapidOrderStatus.parsed, parsedOrder: order), + ); + }, + onError: (String errorKey) => + state.copyWith(status: RapidOrderStatus.failure, error: errorKey), + ); } void _onExampleSelected( RapidOrderExampleSelected event, Emitter emit, ) { - if (state is RapidOrderInitial) { - final String cleanedExample = event.example.replaceAll('"', ''); - emit((state as RapidOrderInitial).copyWith(message: cleanedExample)); - } + final String cleanedExample = event.example.replaceAll('"', ''); + emit( + state.copyWith(message: cleanedExample, status: RapidOrderStatus.initial), + ); } } - diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/rapid_order/rapid_order_state.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/rapid_order/rapid_order_state.dart index 88396c09..af3abd99 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/rapid_order/rapid_order_state.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/rapid_order/rapid_order_state.dart @@ -1,59 +1,55 @@ import 'package:equatable/equatable.dart'; import 'package:krow_domain/krow_domain.dart'; -abstract class RapidOrderState extends Equatable { - const RapidOrderState(); +enum RapidOrderStatus { initial, submitting, parsed, failure } - @override - List get props => []; -} - -class RapidOrderInitial extends RapidOrderState { - const RapidOrderInitial({ +class RapidOrderState extends Equatable { + const RapidOrderState({ + this.status = RapidOrderStatus.initial, this.message = '', this.isListening = false, - required this.examples, + this.isTranscribing = false, + this.examples = const [], + this.error, + this.parsedOrder, }); + + final RapidOrderStatus status; final String message; final bool isListening; + final bool isTranscribing; final List examples; + final String? error; + final OneTimeOrder? parsedOrder; @override - List get props => [message, isListening, examples]; + List get props => [ + status, + message, + isListening, + isTranscribing, + examples, + error, + parsedOrder, + ]; - RapidOrderInitial copyWith({ + RapidOrderState copyWith({ + RapidOrderStatus? status, String? message, bool? isListening, + bool? isTranscribing, List? examples, + String? error, + OneTimeOrder? parsedOrder, }) { - return RapidOrderInitial( + return RapidOrderState( + status: status ?? this.status, message: message ?? this.message, isListening: isListening ?? this.isListening, + isTranscribing: isTranscribing ?? this.isTranscribing, examples: examples ?? this.examples, + error: error ?? this.error, + parsedOrder: parsedOrder ?? this.parsedOrder, ); } } - -class RapidOrderSubmitting extends RapidOrderState { - const RapidOrderSubmitting(); -} - -class RapidOrderSuccess extends RapidOrderState { - const RapidOrderSuccess(); -} - -class RapidOrderFailure extends RapidOrderState { - const RapidOrderFailure(this.error); - final String error; - - @override - List get props => [error]; -} - -class RapidOrderParsed extends RapidOrderState { - const RapidOrderParsed(this.order); - final OneTimeOrder order; - - @override - List get props => [order]; -} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_view.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_view.dart index f306e312..982ba0d2 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_view.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_view.dart @@ -10,7 +10,6 @@ import '../../blocs/rapid_order/rapid_order_event.dart'; import '../../blocs/rapid_order/rapid_order_state.dart'; import 'rapid_order_example_card.dart'; import 'rapid_order_header.dart'; -import 'rapid_order_success_view.dart'; /// The main content of the Rapid Order page. class RapidOrderView extends StatelessWidget { @@ -19,23 +18,7 @@ class RapidOrderView extends StatelessWidget { @override Widget build(BuildContext context) { - final TranslationsClientCreateOrderRapidEn labels = - t.client_create_order.rapid; - - return BlocBuilder( - builder: (BuildContext context, RapidOrderState state) { - if (state is RapidOrderSuccess) { - return RapidOrderSuccessView( - title: labels.success_title, - message: labels.success_message, - buttonLabel: labels.back_to_orders, - onDone: () => Modular.to.toClientOrders(), - ); - } - - return const _RapidOrderForm(); - }, - ); + return const _RapidOrderForm(); } } @@ -65,24 +48,26 @@ class _RapidOrderFormState extends State<_RapidOrderForm> { return BlocListener( listener: (BuildContext context, RapidOrderState state) { - if (state is RapidOrderInitial) { + if (state.status == RapidOrderStatus.initial) { if (_messageController.text != state.message) { _messageController.text = state.message; _messageController.selection = TextSelection.fromPosition( TextPosition(offset: _messageController.text.length), ); } - } else if (state is RapidOrderParsed) { + } else if (state.status == RapidOrderStatus.parsed && + state.parsedOrder != null) { Modular.to.toCreateOrderOneTime( arguments: { - 'order': state.order, + 'order': state.parsedOrder, 'isRapidDraft': true, }, ); - } else if (state is RapidOrderFailure) { + } else if (state.status == RapidOrderStatus.failure && + state.error != null) { UiSnackbar.show( context, - message: translateErrorKey(state.error), + message: translateErrorKey(state.error!), type: UiSnackbarType.error, ); } @@ -100,63 +85,25 @@ class _RapidOrderFormState extends State<_RapidOrderForm> { // Content Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.all(UiConstants.space5), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - labels.tell_us, - style: UiTypography.headline3m.textPrimary, - ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space2, - vertical: UiConstants.space1, - ), - decoration: BoxDecoration( - color: UiColors.destructive, - borderRadius: UiConstants.radiusSm, - ), - child: Text( - labels.urgent_badge, - style: UiTypography.footnote2b.copyWith( - color: UiColors.white, - ), - ), - ), - ], - ), - const SizedBox(height: UiConstants.space4), + child: BlocBuilder( + builder: (BuildContext context, RapidOrderState state) { + final bool isSubmitting = + state.status == RapidOrderStatus.submitting; - // Main Card - Container( - padding: const EdgeInsets.all(UiConstants.space6), - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: UiConstants.radiusLg, - border: Border.all(color: UiColors.border), - ), - child: BlocBuilder( - builder: (BuildContext context, RapidOrderState state) { - final RapidOrderInitial? initialState = - state is RapidOrderInitial ? state : null; - final bool isSubmitting = - state is RapidOrderSubmitting; - - return Column( + return Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( children: [ // Icon const _AnimatedZapIcon(), const SizedBox(height: UiConstants.space4), Text( labels.need_staff, - style: UiTypography.headline2m.textPrimary, + style: UiTypography.headline3b.textPrimary, ), - const SizedBox(height: UiConstants.space2), Text( labels.type_or_speak, textAlign: TextAlign.center, @@ -165,31 +112,30 @@ class _RapidOrderFormState extends State<_RapidOrderForm> { const SizedBox(height: UiConstants.space6), // Examples - if (initialState != null) - ...initialState.examples.asMap().entries.map(( - MapEntry entry, - ) { - final int index = entry.key; - final String example = entry.value; - final bool isHighlighted = index == 0; + ...state.examples.asMap().entries.map(( + MapEntry entry, + ) { + final int index = entry.key; + final String example = entry.value; + final bool isHighlighted = index == 0; - return Padding( - padding: const EdgeInsets.only( - bottom: UiConstants.space2, - ), - child: RapidOrderExampleCard( - example: example, - isHighlighted: isHighlighted, - label: labels.example, - onTap: () => - BlocProvider.of( - context, - ).add( - RapidOrderExampleSelected(example), - ), - ), - ); - }), + return Padding( + padding: const EdgeInsets.only( + bottom: UiConstants.space2, + ), + child: RapidOrderExampleCard( + example: example, + isHighlighted: isHighlighted, + label: labels.example, + onTap: () => + BlocProvider.of( + context, + ).add( + RapidOrderExampleSelected(example), + ), + ), + ); + }), const SizedBox(height: UiConstants.space4), // Input @@ -203,24 +149,23 @@ class _RapidOrderFormState extends State<_RapidOrderForm> { }, hintText: labels.hint, ), - const SizedBox(height: UiConstants.space4), - - // Actions - _RapidOrderActions( - labels: labels, - isSubmitting: isSubmitting, - isListening: initialState?.isListening ?? false, - isMessageEmpty: - initialState != null && - initialState.message.trim().isEmpty, - ), ], - ); - }, + ), + ), ), - ), - ], - ), + Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: _RapidOrderActions( + labels: labels, + isSubmitting: isSubmitting, + isListening: state.isListening, + isTranscribing: state.isTranscribing, + isMessageEmpty: state.message.trim().isEmpty, + ), + ), + ], + ); + }, ), ), ], @@ -248,13 +193,6 @@ class _AnimatedZapIcon extends StatelessWidget { end: Alignment.bottomRight, ), borderRadius: UiConstants.radiusLg, - boxShadow: [ - BoxShadow( - color: UiColors.destructive.withValues(alpha: 0.3), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], ), child: const Icon(UiIcons.zap, color: UiColors.white, size: 32), ); @@ -266,11 +204,13 @@ class _RapidOrderActions extends StatelessWidget { required this.labels, required this.isSubmitting, required this.isListening, + required this.isTranscribing, required this.isMessageEmpty, }); final TranslationsClientCreateOrderRapidEn labels; final bool isSubmitting; final bool isListening; + final bool isTranscribing; final bool isMessageEmpty; @override @@ -279,11 +219,17 @@ class _RapidOrderActions extends StatelessWidget { children: [ Expanded( child: UiButton.secondary( - text: isListening ? labels.listening : labels.speak, + text: isTranscribing + ? labels.transcribing + : isListening + ? labels.listening + : labels.speak, leadingIcon: UiIcons.microphone, - onPressed: () => BlocProvider.of( - context, - ).add(const RapidOrderVoiceToggled()), + onPressed: isTranscribing + ? null + : () => BlocProvider.of( + context, + ).add(const RapidOrderVoiceToggled()), style: OutlinedButton.styleFrom( backgroundColor: isListening ? UiColors.destructive.withValues(alpha: 0.05)