aboutsummaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/grpc/generated/voice_agent.pb.dart667
-rw-r--r--lib/grpc/generated/voice_agent.pbenum.dart121
-rw-r--r--lib/grpc/generated/voice_agent.pbgrpc.dart136
-rw-r--r--lib/grpc/generated/voice_agent.pbjson.dart251
-rw-r--r--lib/grpc/voice_agent_client.dart71
-rw-r--r--lib/main.dart77
-rw-r--r--lib/models/app_state.dart8
-rw-r--r--lib/protos/voice_agent.proto83
-rw-r--r--lib/providers/service_status.dart12
-rw-r--r--lib/screens/error_screen.dart62
-rw-r--r--lib/screens/home_screen.dart398
-rw-r--r--lib/utils/app_config.dart26
-rw-r--r--lib/widgets/assistant_mode_choice.dart202
-rw-r--r--lib/widgets/chat_section.dart128
-rw-r--r--lib/widgets/listen_wake_word_section.dart66
-rw-r--r--lib/widgets/nlu_engine_choice.dart200
-rw-r--r--lib/widgets/record_command_button.dart66
17 files changed, 2574 insertions, 0 deletions
diff --git a/lib/grpc/generated/voice_agent.pb.dart b/lib/grpc/generated/voice_agent.pb.dart
new file mode 100644
index 0000000..363e93f
--- /dev/null
+++ b/lib/grpc/generated/voice_agent.pb.dart
@@ -0,0 +1,667 @@
+//
+// Generated code. Do not modify.
+// source: voice_agent.proto
+//
+// @dart = 2.12
+
+// ignore_for_file: annotate_overrides, camel_case_types, comment_references
+// ignore_for_file: constant_identifier_names, library_prefixes
+// ignore_for_file: non_constant_identifier_names, prefer_final_fields
+// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
+
+import 'dart:core' as $core;
+
+import 'package:protobuf/protobuf.dart' as $pb;
+
+import 'voice_agent.pbenum.dart';
+
+export 'voice_agent.pbenum.dart';
+
+class Empty extends $pb.GeneratedMessage {
+ factory Empty() => create();
+ Empty._() : super();
+ factory Empty.fromBuffer($core.List<$core.int> i,
+ [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
+ create()..mergeFromBuffer(i, r);
+ factory Empty.fromJson($core.String i,
+ [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
+ create()..mergeFromJson(i, r);
+
+ static final $pb.BuilderInfo _i = $pb.BuilderInfo(
+ _omitMessageNames ? '' : 'Empty',
+ createEmptyInstance: create)
+ ..hasRequiredFields = false;
+
+ @$core.Deprecated('Using this can add significant overhead to your binary. '
+ 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+ 'Will be removed in next major version')
+ Empty clone() => Empty()..mergeFromMessage(this);
+ @$core.Deprecated('Using this can add significant overhead to your binary. '
+ 'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+ 'Will be removed in next major version')
+ Empty copyWith(void Function(Empty) updates) =>
+ super.copyWith((message) => updates(message as Empty)) as Empty;
+
+ $pb.BuilderInfo get info_ => _i;
+
+ @$core.pragma('dart2js:noInline')
+ static Empty create() => Empty._();
+ Empty createEmptyInstance() => create();
+ static $pb.PbList<Empty> createRepeated() => $pb.PbList<Empty>();
+ @$core.pragma('dart2js:noInline')
+ static Empty getDefault() =>
+ _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Empty>(create);
+ static Empty? _defaultInstance;
+}
+
+class ServiceStatus extends $pb.GeneratedMessage {
+ factory ServiceStatus({
+ $core.String? version,
+ $core.bool? status,
+ }) {
+ final $result = create();
+ if (version != null) {
+ $result.version = version;
+ }
+ if (status != null) {
+ $result.status = status;
+ }
+ return $result;
+ }
+ ServiceStatus._() : super();
+ factory ServiceStatus.fromBuffer($core.List<$core.int> i,
+ [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
+ create()..mergeFromBuffer(i, r);
+ factory ServiceStatus.fromJson($core.String i,
+ [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
+ create()..mergeFromJson(i, r);
+
+ static final $pb.BuilderInfo _i = $pb.BuilderInfo(
+ _omitMessageNames ? '' : 'ServiceStatus',
+ createEmptyInstance: create)
+ ..aOS(1, _omitFieldNames ? '' : 'version')
+ ..aOB(2, _omitFieldNames ? '' : 'status')
+ ..hasRequiredFields = false;
+
+ @$core.Deprecated('Using this can add significant overhead to your binary. '
+ 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+ 'Will be removed in next major version')
+ ServiceStatus clone() => ServiceStatus()..mergeFromMessage(this);
+ @$core.Deprecated('Using this can add significant overhead to your binary. '
+ 'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+ 'Will be removed in next major version')
+ ServiceStatus copyWith(void Function(ServiceStatus) updates) =>
+ super.copyWith((message) => updates(message as ServiceStatus))
+ as ServiceStatus;
+
+ $pb.BuilderInfo get info_ => _i;
+
+ @$core.pragma('dart2js:noInline')
+ static ServiceStatus create() => ServiceStatus._();
+ ServiceStatus createEmptyInstance() => create();
+ static $pb.PbList<ServiceStatus> createRepeated() =>
+ $pb.PbList<ServiceStatus>();
+ @$core.pragma('dart2js:noInline')
+ static ServiceStatus getDefault() => _defaultInstance ??=
+ $pb.GeneratedMessage.$_defaultFor<ServiceStatus>(create);
+ static ServiceStatus? _defaultInstance;
+
+ @$pb.TagNumber(1)
+ $core.String get version => $_getSZ(0);
+ @$pb.TagNumber(1)
+ set version($core.String v) {
+ $_setString(0, v);
+ }
+
+ @$pb.TagNumber(1)
+ $core.bool hasVersion() => $_has(0);
+ @$pb.TagNumber(1)
+ void clearVersion() => clearField(1);
+
+ @$pb.TagNumber(2)
+ $core.bool get status => $_getBF(1);
+ @$pb.TagNumber(2)
+ set status($core.bool v) {
+ $_setBool(1, v);
+ }
+
+ @$pb.TagNumber(2)
+ $core.bool hasStatus() => $_has(1);
+ @$pb.TagNumber(2)
+ void clearStatus() => clearField(2);
+}
+
+class WakeWordStatus extends $pb.GeneratedMessage {
+ factory WakeWordStatus({
+ $core.bool? status,
+ }) {
+ final $result = create();
+ if (status != null) {
+ $result.status = status;
+ }
+ return $result;
+ }
+ WakeWordStatus._() : super();
+ factory WakeWordStatus.fromBuffer($core.List<$core.int> i,
+ [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
+ create()..mergeFromBuffer(i, r);
+ factory WakeWordStatus.fromJson($core.String i,
+ [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
+ create()..mergeFromJson(i, r);
+
+ static final $pb.BuilderInfo _i = $pb.BuilderInfo(
+ _omitMessageNames ? '' : 'WakeWordStatus',
+ createEmptyInstance: create)
+ ..aOB(1, _omitFieldNames ? '' : 'status')
+ ..hasRequiredFields = false;
+
+ @$core.Deprecated('Using this can add significant overhead to your binary. '
+ 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+ 'Will be removed in next major version')
+ WakeWordStatus clone() => WakeWordStatus()..mergeFromMessage(this);
+ @$core.Deprecated('Using this can add significant overhead to your binary. '
+ 'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+ 'Will be removed in next major version')
+ WakeWordStatus copyWith(void Function(WakeWordStatus) updates) =>
+ super.copyWith((message) => updates(message as WakeWordStatus))
+ as WakeWordStatus;
+
+ $pb.BuilderInfo get info_ => _i;
+
+ @$core.pragma('dart2js:noInline')
+ static WakeWordStatus create() => WakeWordStatus._();
+ WakeWordStatus createEmptyInstance() => create();
+ static $pb.PbList<WakeWordStatus> createRepeated() =>
+ $pb.PbList<WakeWordStatus>();
+ @$core.pragma('dart2js:noInline')
+ static WakeWordStatus getDefault() => _defaultInstance ??=
+ $pb.GeneratedMessage.$_defaultFor<WakeWordStatus>(create);
+ static WakeWordStatus? _defaultInstance;
+
+ @$pb.TagNumber(1)
+ $core.bool get status => $_getBF(0);
+ @$pb.TagNumber(1)
+ set status($core.bool v) {
+ $_setBool(0, v);
+ }
+
+ @$pb.TagNumber(1)
+ $core.bool hasStatus() => $_has(0);
+ @$pb.TagNumber(1)
+ void clearStatus() => clearField(1);
+}
+
+class RecognizeControl extends $pb.GeneratedMessage {
+ factory RecognizeControl({
+ RecordAction? action,
+ NLUModel? nluModel,
+ RecordMode? recordMode,
+ $core.String? streamId,
+ }) {
+ final $result = create();
+ if (action != null) {
+ $result.action = action;
+ }
+ if (nluModel != null) {
+ $result.nluModel = nluModel;
+ }
+ if (recordMode != null) {
+ $result.recordMode = recordMode;
+ }
+ if (streamId != null) {
+ $result.streamId = streamId;
+ }
+ return $result;
+ }
+ RecognizeControl._() : super();
+ factory RecognizeControl.fromBuffer($core.List<$core.int> i,
+ [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
+ create()..mergeFromBuffer(i, r);
+ factory RecognizeControl.fromJson($core.String i,
+ [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
+ create()..mergeFromJson(i, r);
+
+ static final $pb.BuilderInfo _i = $pb.BuilderInfo(
+ _omitMessageNames ? '' : 'RecognizeControl',
+ createEmptyInstance: create)
+ ..e<RecordAction>(1, _omitFieldNames ? '' : 'action', $pb.PbFieldType.OE,
+ defaultOrMaker: RecordAction.START,
+ valueOf: RecordAction.valueOf,
+ enumValues: RecordAction.values)
+ ..e<NLUModel>(2, _omitFieldNames ? '' : 'nluModel', $pb.PbFieldType.OE,
+ defaultOrMaker: NLUModel.SNIPS,
+ valueOf: NLUModel.valueOf,
+ enumValues: NLUModel.values)
+ ..e<RecordMode>(3, _omitFieldNames ? '' : 'recordMode', $pb.PbFieldType.OE,
+ defaultOrMaker: RecordMode.MANUAL,
+ valueOf: RecordMode.valueOf,
+ enumValues: RecordMode.values)
+ ..aOS(4, _omitFieldNames ? '' : 'streamId')
+ ..hasRequiredFields = false;
+
+ @$core.Deprecated('Using this can add significant overhead to your binary. '
+ 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+ 'Will be removed in next major version')
+ RecognizeControl clone() => RecognizeControl()..mergeFromMessage(this);
+ @$core.Deprecated('Using this can add significant overhead to your binary. '
+ 'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+ 'Will be removed in next major version')
+ RecognizeControl copyWith(void Function(RecognizeControl) updates) =>
+ super.copyWith((message) => updates(message as RecognizeControl))
+ as RecognizeControl;
+
+ $pb.BuilderInfo get info_ => _i;
+
+ @$core.pragma('dart2js:noInline')
+ static RecognizeControl create() => RecognizeControl._();
+ RecognizeControl createEmptyInstance() => create();
+ static $pb.PbList<RecognizeControl> createRepeated() =>
+ $pb.PbList<RecognizeControl>();
+ @$core.pragma('dart2js:noInline')
+ static RecognizeControl getDefault() => _defaultInstance ??=
+ $pb.GeneratedMessage.$_defaultFor<RecognizeControl>(create);
+ static RecognizeControl? _defaultInstance;
+
+ @$pb.TagNumber(1)
+ RecordAction get action => $_getN(0);
+ @$pb.TagNumber(1)
+ set action(RecordAction v) {
+ setField(1, v);
+ }
+
+ @$pb.TagNumber(1)
+ $core.bool hasAction() => $_has(0);
+ @$pb.TagNumber(1)
+ void clearAction() => clearField(1);
+
+ @$pb.TagNumber(2)
+ NLUModel get nluModel => $_getN(1);
+ @$pb.TagNumber(2)
+ set nluModel(NLUModel v) {
+ setField(2, v);
+ }
+
+ @$pb.TagNumber(2)
+ $core.bool hasNluModel() => $_has(1);
+ @$pb.TagNumber(2)
+ void clearNluModel() => clearField(2);
+
+ @$pb.TagNumber(3)
+ RecordMode get recordMode => $_getN(2);
+ @$pb.TagNumber(3)
+ set recordMode(RecordMode v) {
+ setField(3, v);
+ }
+
+ @$pb.TagNumber(3)
+ $core.bool hasRecordMode() => $_has(2);
+ @$pb.TagNumber(3)
+ void clearRecordMode() => clearField(3);
+
+ @$pb.TagNumber(4)
+ $core.String get streamId => $_getSZ(3);
+ @$pb.TagNumber(4)
+ set streamId($core.String v) {
+ $_setString(3, v);
+ }
+
+ @$pb.TagNumber(4)
+ $core.bool hasStreamId() => $_has(3);
+ @$pb.TagNumber(4)
+ void clearStreamId() => clearField(4);
+}
+
+class IntentSlot extends $pb.GeneratedMessage {
+ factory IntentSlot({
+ $core.String? name,
+ $core.String? value,
+ }) {
+ final $result = create();
+ if (name != null) {
+ $result.name = name;
+ }
+ if (value != null) {
+ $result.value = value;
+ }
+ return $result;
+ }
+ IntentSlot._() : super();
+ factory IntentSlot.fromBuffer($core.List<$core.int> i,
+ [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
+ create()..mergeFromBuffer(i, r);
+ factory IntentSlot.fromJson($core.String i,
+ [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
+ create()..mergeFromJson(i, r);
+
+ static final $pb.BuilderInfo _i = $pb.BuilderInfo(
+ _omitMessageNames ? '' : 'IntentSlot',
+ createEmptyInstance: create)
+ ..aOS(1, _omitFieldNames ? '' : 'name')
+ ..aOS(2, _omitFieldNames ? '' : 'value')
+ ..hasRequiredFields = false;
+
+ @$core.Deprecated('Using this can add significant overhead to your binary. '
+ 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+ 'Will be removed in next major version')
+ IntentSlot clone() => IntentSlot()..mergeFromMessage(this);
+ @$core.Deprecated('Using this can add significant overhead to your binary. '
+ 'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+ 'Will be removed in next major version')
+ IntentSlot copyWith(void Function(IntentSlot) updates) =>
+ super.copyWith((message) => updates(message as IntentSlot)) as IntentSlot;
+
+ $pb.BuilderInfo get info_ => _i;
+
+ @$core.pragma('dart2js:noInline')
+ static IntentSlot create() => IntentSlot._();
+ IntentSlot createEmptyInstance() => create();
+ static $pb.PbList<IntentSlot> createRepeated() => $pb.PbList<IntentSlot>();
+ @$core.pragma('dart2js:noInline')
+ static IntentSlot getDefault() => _defaultInstance ??=
+ $pb.GeneratedMessage.$_defaultFor<IntentSlot>(create);
+ static IntentSlot? _defaultInstance;
+
+ @$pb.TagNumber(1)
+ $core.String get name => $_getSZ(0);
+ @$pb.TagNumber(1)
+ set name($core.String v) {
+ $_setString(0, v);
+ }
+
+ @$pb.TagNumber(1)
+ $core.bool hasName() => $_has(0);
+ @$pb.TagNumber(1)
+ void clearName() => clearField(1);
+
+ @$pb.TagNumber(2)
+ $core.String get value => $_getSZ(1);
+ @$pb.TagNumber(2)
+ set value($core.String v) {
+ $_setString(1, v);
+ }
+
+ @$pb.TagNumber(2)
+ $core.bool hasValue() => $_has(1);
+ @$pb.TagNumber(2)
+ void clearValue() => clearField(2);
+}
+
+class RecognizeResult extends $pb.GeneratedMessage {
+ factory RecognizeResult({
+ $core.String? command,
+ $core.String? intent,
+ $core.Iterable<IntentSlot>? intentSlots,
+ $core.String? streamId,
+ RecognizeStatusType? status,
+ }) {
+ final $result = create();
+ if (command != null) {
+ $result.command = command;
+ }
+ if (intent != null) {
+ $result.intent = intent;
+ }
+ if (intentSlots != null) {
+ $result.intentSlots.addAll(intentSlots);
+ }
+ if (streamId != null) {
+ $result.streamId = streamId;
+ }
+ if (status != null) {
+ $result.status = status;
+ }
+ return $result;
+ }
+ RecognizeResult._() : super();
+ factory RecognizeResult.fromBuffer($core.List<$core.int> i,
+ [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
+ create()..mergeFromBuffer(i, r);
+ factory RecognizeResult.fromJson($core.String i,
+ [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
+ create()..mergeFromJson(i, r);
+
+ static final $pb.BuilderInfo _i = $pb.BuilderInfo(
+ _omitMessageNames ? '' : 'RecognizeResult',
+ createEmptyInstance: create)
+ ..aOS(1, _omitFieldNames ? '' : 'command')
+ ..aOS(2, _omitFieldNames ? '' : 'intent')
+ ..pc<IntentSlot>(
+ 3, _omitFieldNames ? '' : 'intentSlots', $pb.PbFieldType.PM,
+ subBuilder: IntentSlot.create)
+ ..aOS(4, _omitFieldNames ? '' : 'streamId')
+ ..e<RecognizeStatusType>(
+ 5, _omitFieldNames ? '' : 'status', $pb.PbFieldType.OE,
+ defaultOrMaker: RecognizeStatusType.REC_ERROR,
+ valueOf: RecognizeStatusType.valueOf,
+ enumValues: RecognizeStatusType.values)
+ ..hasRequiredFields = false;
+
+ @$core.Deprecated('Using this can add significant overhead to your binary. '
+ 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+ 'Will be removed in next major version')
+ RecognizeResult clone() => RecognizeResult()..mergeFromMessage(this);
+ @$core.Deprecated('Using this can add significant overhead to your binary. '
+ 'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+ 'Will be removed in next major version')
+ RecognizeResult copyWith(void Function(RecognizeResult) updates) =>
+ super.copyWith((message) => updates(message as RecognizeResult))
+ as RecognizeResult;
+
+ $pb.BuilderInfo get info_ => _i;
+
+ @$core.pragma('dart2js:noInline')
+ static RecognizeResult create() => RecognizeResult._();
+ RecognizeResult createEmptyInstance() => create();
+ static $pb.PbList<RecognizeResult> createRepeated() =>
+ $pb.PbList<RecognizeResult>();
+ @$core.pragma('dart2js:noInline')
+ static RecognizeResult getDefault() => _defaultInstance ??=
+ $pb.GeneratedMessage.$_defaultFor<RecognizeResult>(create);
+ static RecognizeResult? _defaultInstance;
+
+ @$pb.TagNumber(1)
+ $core.String get command => $_getSZ(0);
+ @$pb.TagNumber(1)
+ set command($core.String v) {
+ $_setString(0, v);
+ }
+
+ @$pb.TagNumber(1)
+ $core.bool hasCommand() => $_has(0);
+ @$pb.TagNumber(1)
+ void clearCommand() => clearField(1);
+
+ @$pb.TagNumber(2)
+ $core.String get intent => $_getSZ(1);
+ @$pb.TagNumber(2)
+ set intent($core.String v) {
+ $_setString(1, v);
+ }
+
+ @$pb.TagNumber(2)
+ $core.bool hasIntent() => $_has(1);
+ @$pb.TagNumber(2)
+ void clearIntent() => clearField(2);
+
+ @$pb.TagNumber(3)
+ $core.List<IntentSlot> get intentSlots => $_getList(2);
+
+ @$pb.TagNumber(4)
+ $core.String get streamId => $_getSZ(3);
+ @$pb.TagNumber(4)
+ set streamId($core.String v) {
+ $_setString(3, v);
+ }
+
+ @$pb.TagNumber(4)
+ $core.bool hasStreamId() => $_has(3);
+ @$pb.TagNumber(4)
+ void clearStreamId() => clearField(4);
+
+ @$pb.TagNumber(5)
+ RecognizeStatusType get status => $_getN(4);
+ @$pb.TagNumber(5)
+ set status(RecognizeStatusType v) {
+ setField(5, v);
+ }
+
+ @$pb.TagNumber(5)
+ $core.bool hasStatus() => $_has(4);
+ @$pb.TagNumber(5)
+ void clearStatus() => clearField(5);
+}
+
+class ExecuteInput extends $pb.GeneratedMessage {
+ factory ExecuteInput({
+ $core.String? intent,
+ $core.Iterable<IntentSlot>? intentSlots,
+ }) {
+ final $result = create();
+ if (intent != null) {
+ $result.intent = intent;
+ }
+ if (intentSlots != null) {
+ $result.intentSlots.addAll(intentSlots);
+ }
+ return $result;
+ }
+ ExecuteInput._() : super();
+ factory ExecuteInput.fromBuffer($core.List<$core.int> i,
+ [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
+ create()..mergeFromBuffer(i, r);
+ factory ExecuteInput.fromJson($core.String i,
+ [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
+ create()..mergeFromJson(i, r);
+
+ static final $pb.BuilderInfo _i = $pb.BuilderInfo(
+ _omitMessageNames ? '' : 'ExecuteInput',
+ createEmptyInstance: create)
+ ..aOS(1, _omitFieldNames ? '' : 'intent')
+ ..pc<IntentSlot>(
+ 2, _omitFieldNames ? '' : 'intentSlots', $pb.PbFieldType.PM,
+ subBuilder: IntentSlot.create)
+ ..hasRequiredFields = false;
+
+ @$core.Deprecated('Using this can add significant overhead to your binary. '
+ 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+ 'Will be removed in next major version')
+ ExecuteInput clone() => ExecuteInput()..mergeFromMessage(this);
+ @$core.Deprecated('Using this can add significant overhead to your binary. '
+ 'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+ 'Will be removed in next major version')
+ ExecuteInput copyWith(void Function(ExecuteInput) updates) =>
+ super.copyWith((message) => updates(message as ExecuteInput))
+ as ExecuteInput;
+
+ $pb.BuilderInfo get info_ => _i;
+
+ @$core.pragma('dart2js:noInline')
+ static ExecuteInput create() => ExecuteInput._();
+ ExecuteInput createEmptyInstance() => create();
+ static $pb.PbList<ExecuteInput> createRepeated() =>
+ $pb.PbList<ExecuteInput>();
+ @$core.pragma('dart2js:noInline')
+ static ExecuteInput getDefault() => _defaultInstance ??=
+ $pb.GeneratedMessage.$_defaultFor<ExecuteInput>(create);
+ static ExecuteInput? _defaultInstance;
+
+ @$pb.TagNumber(1)
+ $core.String get intent => $_getSZ(0);
+ @$pb.TagNumber(1)
+ set intent($core.String v) {
+ $_setString(0, v);
+ }
+
+ @$pb.TagNumber(1)
+ $core.bool hasIntent() => $_has(0);
+ @$pb.TagNumber(1)
+ void clearIntent() => clearField(1);
+
+ @$pb.TagNumber(2)
+ $core.List<IntentSlot> get intentSlots => $_getList(1);
+}
+
+class ExecuteResult extends $pb.GeneratedMessage {
+ factory ExecuteResult({
+ $core.String? response,
+ ExecuteStatusType? status,
+ }) {
+ final $result = create();
+ if (response != null) {
+ $result.response = response;
+ }
+ if (status != null) {
+ $result.status = status;
+ }
+ return $result;
+ }
+ ExecuteResult._() : super();
+ factory ExecuteResult.fromBuffer($core.List<$core.int> i,
+ [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
+ create()..mergeFromBuffer(i, r);
+ factory ExecuteResult.fromJson($core.String i,
+ [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
+ create()..mergeFromJson(i, r);
+
+ static final $pb.BuilderInfo _i = $pb.BuilderInfo(
+ _omitMessageNames ? '' : 'ExecuteResult',
+ createEmptyInstance: create)
+ ..aOS(1, _omitFieldNames ? '' : 'response')
+ ..e<ExecuteStatusType>(
+ 2, _omitFieldNames ? '' : 'status', $pb.PbFieldType.OE,
+ defaultOrMaker: ExecuteStatusType.EXEC_ERROR,
+ valueOf: ExecuteStatusType.valueOf,
+ enumValues: ExecuteStatusType.values)
+ ..hasRequiredFields = false;
+
+ @$core.Deprecated('Using this can add significant overhead to your binary. '
+ 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+ 'Will be removed in next major version')
+ ExecuteResult clone() => ExecuteResult()..mergeFromMessage(this);
+ @$core.Deprecated('Using this can add significant overhead to your binary. '
+ 'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+ 'Will be removed in next major version')
+ ExecuteResult copyWith(void Function(ExecuteResult) updates) =>
+ super.copyWith((message) => updates(message as ExecuteResult))
+ as ExecuteResult;
+
+ $pb.BuilderInfo get info_ => _i;
+
+ @$core.pragma('dart2js:noInline')
+ static ExecuteResult create() => ExecuteResult._();
+ ExecuteResult createEmptyInstance() => create();
+ static $pb.PbList<ExecuteResult> createRepeated() =>
+ $pb.PbList<ExecuteResult>();
+ @$core.pragma('dart2js:noInline')
+ static ExecuteResult getDefault() => _defaultInstance ??=
+ $pb.GeneratedMessage.$_defaultFor<ExecuteResult>(create);
+ static ExecuteResult? _defaultInstance;
+
+ @$pb.TagNumber(1)
+ $core.String get response => $_getSZ(0);
+ @$pb.TagNumber(1)
+ set response($core.String v) {
+ $_setString(0, v);
+ }
+
+ @$pb.TagNumber(1)
+ $core.bool hasResponse() => $_has(0);
+ @$pb.TagNumber(1)
+ void clearResponse() => clearField(1);
+
+ @$pb.TagNumber(2)
+ ExecuteStatusType get status => $_getN(1);
+ @$pb.TagNumber(2)
+ set status(ExecuteStatusType v) {
+ setField(2, v);
+ }
+
+ @$pb.TagNumber(2)
+ $core.bool hasStatus() => $_has(1);
+ @$pb.TagNumber(2)
+ void clearStatus() => clearField(2);
+}
+
+const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names');
+const _omitMessageNames =
+ $core.bool.fromEnvironment('protobuf.omit_message_names');
diff --git a/lib/grpc/generated/voice_agent.pbenum.dart b/lib/grpc/generated/voice_agent.pbenum.dart
new file mode 100644
index 0000000..a001f03
--- /dev/null
+++ b/lib/grpc/generated/voice_agent.pbenum.dart
@@ -0,0 +1,121 @@
+//
+// Generated code. Do not modify.
+// source: voice_agent.proto
+//
+// @dart = 2.12
+
+// ignore_for_file: annotate_overrides, camel_case_types, comment_references
+// ignore_for_file: constant_identifier_names, library_prefixes
+// ignore_for_file: non_constant_identifier_names, prefer_final_fields
+// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
+
+import 'dart:core' as $core;
+
+import 'package:protobuf/protobuf.dart' as $pb;
+
+class RecordAction extends $pb.ProtobufEnum {
+ static const RecordAction START =
+ RecordAction._(0, _omitEnumNames ? '' : 'START');
+ static const RecordAction STOP =
+ RecordAction._(1, _omitEnumNames ? '' : 'STOP');
+
+ static const $core.List<RecordAction> values = <RecordAction>[
+ START,
+ STOP,
+ ];
+
+ static final $core.Map<$core.int, RecordAction> _byValue =
+ $pb.ProtobufEnum.initByValue(values);
+ static RecordAction? valueOf($core.int value) => _byValue[value];
+
+ const RecordAction._($core.int v, $core.String n) : super(v, n);
+}
+
+class NLUModel extends $pb.ProtobufEnum {
+ static const NLUModel SNIPS = NLUModel._(0, _omitEnumNames ? '' : 'SNIPS');
+ static const NLUModel RASA = NLUModel._(1, _omitEnumNames ? '' : 'RASA');
+
+ static const $core.List<NLUModel> values = <NLUModel>[
+ SNIPS,
+ RASA,
+ ];
+
+ static final $core.Map<$core.int, NLUModel> _byValue =
+ $pb.ProtobufEnum.initByValue(values);
+ static NLUModel? valueOf($core.int value) => _byValue[value];
+
+ const NLUModel._($core.int v, $core.String n) : super(v, n);
+}
+
+class RecordMode extends $pb.ProtobufEnum {
+ static const RecordMode MANUAL =
+ RecordMode._(0, _omitEnumNames ? '' : 'MANUAL');
+ static const RecordMode AUTO = RecordMode._(1, _omitEnumNames ? '' : 'AUTO');
+
+ static const $core.List<RecordMode> values = <RecordMode>[
+ MANUAL,
+ AUTO,
+ ];
+
+ static final $core.Map<$core.int, RecordMode> _byValue =
+ $pb.ProtobufEnum.initByValue(values);
+ static RecordMode? valueOf($core.int value) => _byValue[value];
+
+ const RecordMode._($core.int v, $core.String n) : super(v, n);
+}
+
+class RecognizeStatusType extends $pb.ProtobufEnum {
+ static const RecognizeStatusType REC_ERROR =
+ RecognizeStatusType._(0, _omitEnumNames ? '' : 'REC_ERROR');
+ static const RecognizeStatusType REC_SUCCESS =
+ RecognizeStatusType._(1, _omitEnumNames ? '' : 'REC_SUCCESS');
+ static const RecognizeStatusType REC_PROCESSING =
+ RecognizeStatusType._(2, _omitEnumNames ? '' : 'REC_PROCESSING');
+ static const RecognizeStatusType VOICE_NOT_RECOGNIZED =
+ RecognizeStatusType._(3, _omitEnumNames ? '' : 'VOICE_NOT_RECOGNIZED');
+ static const RecognizeStatusType INTENT_NOT_RECOGNIZED =
+ RecognizeStatusType._(4, _omitEnumNames ? '' : 'INTENT_NOT_RECOGNIZED');
+
+ static const $core.List<RecognizeStatusType> values = <RecognizeStatusType>[
+ REC_ERROR,
+ REC_SUCCESS,
+ REC_PROCESSING,
+ VOICE_NOT_RECOGNIZED,
+ INTENT_NOT_RECOGNIZED,
+ ];
+
+ static final $core.Map<$core.int, RecognizeStatusType> _byValue =
+ $pb.ProtobufEnum.initByValue(values);
+ static RecognizeStatusType? valueOf($core.int value) => _byValue[value];
+
+ const RecognizeStatusType._($core.int v, $core.String n) : super(v, n);
+}
+
+class ExecuteStatusType extends $pb.ProtobufEnum {
+ static const ExecuteStatusType EXEC_ERROR =
+ ExecuteStatusType._(0, _omitEnumNames ? '' : 'EXEC_ERROR');
+ static const ExecuteStatusType EXEC_SUCCESS =
+ ExecuteStatusType._(1, _omitEnumNames ? '' : 'EXEC_SUCCESS');
+ static const ExecuteStatusType KUKSA_CONN_ERROR =
+ ExecuteStatusType._(2, _omitEnumNames ? '' : 'KUKSA_CONN_ERROR');
+ static const ExecuteStatusType INTENT_NOT_SUPPORTED =
+ ExecuteStatusType._(3, _omitEnumNames ? '' : 'INTENT_NOT_SUPPORTED');
+ static const ExecuteStatusType INTENT_SLOTS_INCOMPLETE =
+ ExecuteStatusType._(4, _omitEnumNames ? '' : 'INTENT_SLOTS_INCOMPLETE');
+
+ static const $core.List<ExecuteStatusType> values = <ExecuteStatusType>[
+ EXEC_ERROR,
+ EXEC_SUCCESS,
+ KUKSA_CONN_ERROR,
+ INTENT_NOT_SUPPORTED,
+ INTENT_SLOTS_INCOMPLETE,
+ ];
+
+ static final $core.Map<$core.int, ExecuteStatusType> _byValue =
+ $pb.ProtobufEnum.initByValue(values);
+ static ExecuteStatusType? valueOf($core.int value) => _byValue[value];
+
+ const ExecuteStatusType._($core.int v, $core.String n) : super(v, n);
+}
+
+const _omitEnumNames = $core.bool.fromEnvironment('protobuf.omit_enum_names');
diff --git a/lib/grpc/generated/voice_agent.pbgrpc.dart b/lib/grpc/generated/voice_agent.pbgrpc.dart
new file mode 100644
index 0000000..7984ed3
--- /dev/null
+++ b/lib/grpc/generated/voice_agent.pbgrpc.dart
@@ -0,0 +1,136 @@
+//
+// Generated code. Do not modify.
+// source: voice_agent.proto
+//
+// @dart = 2.12
+
+// ignore_for_file: annotate_overrides, camel_case_types, comment_references
+// ignore_for_file: constant_identifier_names, library_prefixes
+// ignore_for_file: non_constant_identifier_names, prefer_final_fields
+// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
+
+import 'dart:async' as $async;
+import 'dart:core' as $core;
+
+import 'package:grpc/service_api.dart' as $grpc;
+import 'package:protobuf/protobuf.dart' as $pb;
+
+import 'voice_agent.pb.dart' as $0;
+
+export 'voice_agent.pb.dart';
+
+// @$pb.GrpcServiceName('VoiceAgentService')
+class VoiceAgentServiceClient extends $grpc.Client {
+ static final _$checkServiceStatus =
+ $grpc.ClientMethod<$0.Empty, $0.ServiceStatus>(
+ '/VoiceAgentService/CheckServiceStatus',
+ ($0.Empty value) => value.writeToBuffer(),
+ ($core.List<$core.int> value) => $0.ServiceStatus.fromBuffer(value));
+ static final _$detectWakeWord =
+ $grpc.ClientMethod<$0.Empty, $0.WakeWordStatus>(
+ '/VoiceAgentService/DetectWakeWord',
+ ($0.Empty value) => value.writeToBuffer(),
+ ($core.List<$core.int> value) => $0.WakeWordStatus.fromBuffer(value));
+ static final _$recognizeVoiceCommand =
+ $grpc.ClientMethod<$0.RecognizeControl, $0.RecognizeResult>(
+ '/VoiceAgentService/RecognizeVoiceCommand',
+ ($0.RecognizeControl value) => value.writeToBuffer(),
+ ($core.List<$core.int> value) =>
+ $0.RecognizeResult.fromBuffer(value));
+ static final _$executeVoiceCommand =
+ $grpc.ClientMethod<$0.ExecuteInput, $0.ExecuteResult>(
+ '/VoiceAgentService/ExecuteVoiceCommand',
+ ($0.ExecuteInput value) => value.writeToBuffer(),
+ ($core.List<$core.int> value) => $0.ExecuteResult.fromBuffer(value));
+
+ VoiceAgentServiceClient($grpc.ClientChannel channel,
+ {$grpc.CallOptions? options,
+ $core.Iterable<$grpc.ClientInterceptor>? interceptors})
+ : super(channel, options: options, interceptors: interceptors);
+
+ $grpc.ResponseFuture<$0.ServiceStatus> checkServiceStatus($0.Empty request,
+ {$grpc.CallOptions? options}) {
+ return $createUnaryCall(_$checkServiceStatus, request, options: options);
+ }
+
+ $grpc.ResponseStream<$0.WakeWordStatus> detectWakeWord($0.Empty request,
+ {$grpc.CallOptions? options}) {
+ return $createStreamingCall(
+ _$detectWakeWord, $async.Stream.fromIterable([request]),
+ options: options);
+ }
+
+ $grpc.ResponseFuture<$0.RecognizeResult> recognizeVoiceCommand(
+ $async.Stream<$0.RecognizeControl> request,
+ {$grpc.CallOptions? options}) {
+ return $createStreamingCall(_$recognizeVoiceCommand, request,
+ options: options)
+ .single;
+ }
+
+ $grpc.ResponseFuture<$0.ExecuteResult> executeVoiceCommand(
+ $0.ExecuteInput request,
+ {$grpc.CallOptions? options}) {
+ return $createUnaryCall(_$executeVoiceCommand, request, options: options);
+ }
+}
+
+// @$pb.GrpcServiceName('VoiceAgentService')
+abstract class VoiceAgentServiceBase extends $grpc.Service {
+ $core.String get $name => 'VoiceAgentService';
+
+ VoiceAgentServiceBase() {
+ $addMethod($grpc.ServiceMethod<$0.Empty, $0.ServiceStatus>(
+ 'CheckServiceStatus',
+ checkServiceStatus_Pre,
+ false,
+ false,
+ ($core.List<$core.int> value) => $0.Empty.fromBuffer(value),
+ ($0.ServiceStatus value) => value.writeToBuffer()));
+ $addMethod($grpc.ServiceMethod<$0.Empty, $0.WakeWordStatus>(
+ 'DetectWakeWord',
+ detectWakeWord_Pre,
+ false,
+ true,
+ ($core.List<$core.int> value) => $0.Empty.fromBuffer(value),
+ ($0.WakeWordStatus value) => value.writeToBuffer()));
+ $addMethod($grpc.ServiceMethod<$0.RecognizeControl, $0.RecognizeResult>(
+ 'RecognizeVoiceCommand',
+ recognizeVoiceCommand,
+ true,
+ false,
+ ($core.List<$core.int> value) => $0.RecognizeControl.fromBuffer(value),
+ ($0.RecognizeResult value) => value.writeToBuffer()));
+ $addMethod($grpc.ServiceMethod<$0.ExecuteInput, $0.ExecuteResult>(
+ 'ExecuteVoiceCommand',
+ executeVoiceCommand_Pre,
+ false,
+ false,
+ ($core.List<$core.int> value) => $0.ExecuteInput.fromBuffer(value),
+ ($0.ExecuteResult value) => value.writeToBuffer()));
+ }
+
+ $async.Future<$0.ServiceStatus> checkServiceStatus_Pre(
+ $grpc.ServiceCall call, $async.Future<$0.Empty> request) async {
+ return checkServiceStatus(call, await request);
+ }
+
+ $async.Stream<$0.WakeWordStatus> detectWakeWord_Pre(
+ $grpc.ServiceCall call, $async.Future<$0.Empty> request) async* {
+ yield* detectWakeWord(call, await request);
+ }
+
+ $async.Future<$0.ExecuteResult> executeVoiceCommand_Pre(
+ $grpc.ServiceCall call, $async.Future<$0.ExecuteInput> request) async {
+ return executeVoiceCommand(call, await request);
+ }
+
+ $async.Future<$0.ServiceStatus> checkServiceStatus(
+ $grpc.ServiceCall call, $0.Empty request);
+ $async.Stream<$0.WakeWordStatus> detectWakeWord(
+ $grpc.ServiceCall call, $0.Empty request);
+ $async.Future<$0.RecognizeResult> recognizeVoiceCommand(
+ $grpc.ServiceCall call, $async.Stream<$0.RecognizeControl> request);
+ $async.Future<$0.ExecuteResult> executeVoiceCommand(
+ $grpc.ServiceCall call, $0.ExecuteInput request);
+}
diff --git a/lib/grpc/generated/voice_agent.pbjson.dart b/lib/grpc/generated/voice_agent.pbjson.dart
new file mode 100644
index 0000000..f4a913c
--- /dev/null
+++ b/lib/grpc/generated/voice_agent.pbjson.dart
@@ -0,0 +1,251 @@
+//
+// Generated code. Do not modify.
+// source: voice_agent.proto
+//
+// @dart = 2.12
+
+// ignore_for_file: annotate_overrides, camel_case_types, comment_references
+// ignore_for_file: constant_identifier_names, library_prefixes
+// ignore_for_file: non_constant_identifier_names, prefer_final_fields
+// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
+
+import 'dart:convert' as $convert;
+import 'dart:core' as $core;
+import 'dart:typed_data' as $typed_data;
+
+@$core.Deprecated('Use recordActionDescriptor instead')
+const RecordAction$json = {
+ '1': 'RecordAction',
+ '2': [
+ {'1': 'START', '2': 0},
+ {'1': 'STOP', '2': 1},
+ ],
+};
+
+/// Descriptor for `RecordAction`. Decode as a `google.protobuf.EnumDescriptorProto`.
+final $typed_data.Uint8List recordActionDescriptor =
+ $convert.base64Decode('CgxSZWNvcmRBY3Rpb24SCQoFU1RBUlQQABIICgRTVE9QEAE=');
+
+@$core.Deprecated('Use nLUModelDescriptor instead')
+const NLUModel$json = {
+ '1': 'NLUModel',
+ '2': [
+ {'1': 'SNIPS', '2': 0},
+ {'1': 'RASA', '2': 1},
+ ],
+};
+
+/// Descriptor for `NLUModel`. Decode as a `google.protobuf.EnumDescriptorProto`.
+final $typed_data.Uint8List nLUModelDescriptor =
+ $convert.base64Decode('CghOTFVNb2RlbBIJCgVTTklQUxAAEggKBFJBU0EQAQ==');
+
+@$core.Deprecated('Use recordModeDescriptor instead')
+const RecordMode$json = {
+ '1': 'RecordMode',
+ '2': [
+ {'1': 'MANUAL', '2': 0},
+ {'1': 'AUTO', '2': 1},
+ ],
+};
+
+/// Descriptor for `RecordMode`. Decode as a `google.protobuf.EnumDescriptorProto`.
+final $typed_data.Uint8List recordModeDescriptor =
+ $convert.base64Decode('CgpSZWNvcmRNb2RlEgoKBk1BTlVBTBAAEggKBEFVVE8QAQ==');
+
+@$core.Deprecated('Use recognizeStatusTypeDescriptor instead')
+const RecognizeStatusType$json = {
+ '1': 'RecognizeStatusType',
+ '2': [
+ {'1': 'REC_ERROR', '2': 0},
+ {'1': 'REC_SUCCESS', '2': 1},
+ {'1': 'REC_PROCESSING', '2': 2},
+ {'1': 'VOICE_NOT_RECOGNIZED', '2': 3},
+ {'1': 'INTENT_NOT_RECOGNIZED', '2': 4},
+ ],
+};
+
+/// Descriptor for `RecognizeStatusType`. Decode as a `google.protobuf.EnumDescriptorProto`.
+final $typed_data.Uint8List recognizeStatusTypeDescriptor = $convert.base64Decode(
+ 'ChNSZWNvZ25pemVTdGF0dXNUeXBlEg0KCVJFQ19FUlJPUhAAEg8KC1JFQ19TVUNDRVNTEAESEg'
+ 'oOUkVDX1BST0NFU1NJTkcQAhIYChRWT0lDRV9OT1RfUkVDT0dOSVpFRBADEhkKFUlOVEVOVF9O'
+ 'T1RfUkVDT0dOSVpFRBAE');
+
+@$core.Deprecated('Use executeStatusTypeDescriptor instead')
+const ExecuteStatusType$json = {
+ '1': 'ExecuteStatusType',
+ '2': [
+ {'1': 'EXEC_ERROR', '2': 0},
+ {'1': 'EXEC_SUCCESS', '2': 1},
+ {'1': 'KUKSA_CONN_ERROR', '2': 2},
+ {'1': 'INTENT_NOT_SUPPORTED', '2': 3},
+ {'1': 'INTENT_SLOTS_INCOMPLETE', '2': 4},
+ ],
+};
+
+/// Descriptor for `ExecuteStatusType`. Decode as a `google.protobuf.EnumDescriptorProto`.
+final $typed_data.Uint8List executeStatusTypeDescriptor = $convert.base64Decode(
+ 'ChFFeGVjdXRlU3RhdHVzVHlwZRIOCgpFWEVDX0VSUk9SEAASEAoMRVhFQ19TVUNDRVNTEAESFA'
+ 'oQS1VLU0FfQ09OTl9FUlJPUhACEhgKFElOVEVOVF9OT1RfU1VQUE9SVEVEEAMSGwoXSU5URU5U'
+ 'X1NMT1RTX0lOQ09NUExFVEUQBA==');
+
+@$core.Deprecated('Use emptyDescriptor instead')
+const Empty$json = {
+ '1': 'Empty',
+};
+
+/// Descriptor for `Empty`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List emptyDescriptor =
+ $convert.base64Decode('CgVFbXB0eQ==');
+
+@$core.Deprecated('Use serviceStatusDescriptor instead')
+const ServiceStatus$json = {
+ '1': 'ServiceStatus',
+ '2': [
+ {'1': 'version', '3': 1, '4': 1, '5': 9, '10': 'version'},
+ {'1': 'status', '3': 2, '4': 1, '5': 8, '10': 'status'},
+ ],
+};
+
+/// Descriptor for `ServiceStatus`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List serviceStatusDescriptor = $convert.base64Decode(
+ 'Cg1TZXJ2aWNlU3RhdHVzEhgKB3ZlcnNpb24YASABKAlSB3ZlcnNpb24SFgoGc3RhdHVzGAIgAS'
+ 'gIUgZzdGF0dXM=');
+
+@$core.Deprecated('Use wakeWordStatusDescriptor instead')
+const WakeWordStatus$json = {
+ '1': 'WakeWordStatus',
+ '2': [
+ {'1': 'status', '3': 1, '4': 1, '5': 8, '10': 'status'},
+ ],
+};
+
+/// Descriptor for `WakeWordStatus`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List wakeWordStatusDescriptor = $convert
+ .base64Decode('Cg5XYWtlV29yZFN0YXR1cxIWCgZzdGF0dXMYASABKAhSBnN0YXR1cw==');
+
+@$core.Deprecated('Use recognizeControlDescriptor instead')
+const RecognizeControl$json = {
+ '1': 'RecognizeControl',
+ '2': [
+ {
+ '1': 'action',
+ '3': 1,
+ '4': 1,
+ '5': 14,
+ '6': '.RecordAction',
+ '10': 'action'
+ },
+ {
+ '1': 'nlu_model',
+ '3': 2,
+ '4': 1,
+ '5': 14,
+ '6': '.NLUModel',
+ '10': 'nluModel'
+ },
+ {
+ '1': 'record_mode',
+ '3': 3,
+ '4': 1,
+ '5': 14,
+ '6': '.RecordMode',
+ '10': 'recordMode'
+ },
+ {'1': 'stream_id', '3': 4, '4': 1, '5': 9, '10': 'streamId'},
+ ],
+};
+
+/// Descriptor for `RecognizeControl`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List recognizeControlDescriptor = $convert.base64Decode(
+ 'ChBSZWNvZ25pemVDb250cm9sEiUKBmFjdGlvbhgBIAEoDjINLlJlY29yZEFjdGlvblIGYWN0aW'
+ '9uEiYKCW5sdV9tb2RlbBgCIAEoDjIJLk5MVU1vZGVsUghubHVNb2RlbBIsCgtyZWNvcmRfbW9k'
+ 'ZRgDIAEoDjILLlJlY29yZE1vZGVSCnJlY29yZE1vZGUSGwoJc3RyZWFtX2lkGAQgASgJUghzdH'
+ 'JlYW1JZA==');
+
+@$core.Deprecated('Use intentSlotDescriptor instead')
+const IntentSlot$json = {
+ '1': 'IntentSlot',
+ '2': [
+ {'1': 'name', '3': 1, '4': 1, '5': 9, '10': 'name'},
+ {'1': 'value', '3': 2, '4': 1, '5': 9, '10': 'value'},
+ ],
+};
+
+/// Descriptor for `IntentSlot`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List intentSlotDescriptor = $convert.base64Decode(
+ 'CgpJbnRlbnRTbG90EhIKBG5hbWUYASABKAlSBG5hbWUSFAoFdmFsdWUYAiABKAlSBXZhbHVl');
+
+@$core.Deprecated('Use recognizeResultDescriptor instead')
+const RecognizeResult$json = {
+ '1': 'RecognizeResult',
+ '2': [
+ {'1': 'command', '3': 1, '4': 1, '5': 9, '10': 'command'},
+ {'1': 'intent', '3': 2, '4': 1, '5': 9, '10': 'intent'},
+ {
+ '1': 'intent_slots',
+ '3': 3,
+ '4': 3,
+ '5': 11,
+ '6': '.IntentSlot',
+ '10': 'intentSlots'
+ },
+ {'1': 'stream_id', '3': 4, '4': 1, '5': 9, '10': 'streamId'},
+ {
+ '1': 'status',
+ '3': 5,
+ '4': 1,
+ '5': 14,
+ '6': '.RecognizeStatusType',
+ '10': 'status'
+ },
+ ],
+};
+
+/// Descriptor for `RecognizeResult`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List recognizeResultDescriptor = $convert.base64Decode(
+ 'Cg9SZWNvZ25pemVSZXN1bHQSGAoHY29tbWFuZBgBIAEoCVIHY29tbWFuZBIWCgZpbnRlbnQYAi'
+ 'ABKAlSBmludGVudBIuCgxpbnRlbnRfc2xvdHMYAyADKAsyCy5JbnRlbnRTbG90UgtpbnRlbnRT'
+ 'bG90cxIbCglzdHJlYW1faWQYBCABKAlSCHN0cmVhbUlkEiwKBnN0YXR1cxgFIAEoDjIULlJlY2'
+ '9nbml6ZVN0YXR1c1R5cGVSBnN0YXR1cw==');
+
+@$core.Deprecated('Use executeInputDescriptor instead')
+const ExecuteInput$json = {
+ '1': 'ExecuteInput',
+ '2': [
+ {'1': 'intent', '3': 1, '4': 1, '5': 9, '10': 'intent'},
+ {
+ '1': 'intent_slots',
+ '3': 2,
+ '4': 3,
+ '5': 11,
+ '6': '.IntentSlot',
+ '10': 'intentSlots'
+ },
+ ],
+};
+
+/// Descriptor for `ExecuteInput`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List executeInputDescriptor = $convert.base64Decode(
+ 'CgxFeGVjdXRlSW5wdXQSFgoGaW50ZW50GAEgASgJUgZpbnRlbnQSLgoMaW50ZW50X3Nsb3RzGA'
+ 'IgAygLMgsuSW50ZW50U2xvdFILaW50ZW50U2xvdHM=');
+
+@$core.Deprecated('Use executeResultDescriptor instead')
+const ExecuteResult$json = {
+ '1': 'ExecuteResult',
+ '2': [
+ {'1': 'response', '3': 1, '4': 1, '5': 9, '10': 'response'},
+ {
+ '1': 'status',
+ '3': 2,
+ '4': 1,
+ '5': 14,
+ '6': '.ExecuteStatusType',
+ '10': 'status'
+ },
+ ],
+};
+
+/// Descriptor for `ExecuteResult`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List executeResultDescriptor = $convert.base64Decode(
+ 'Cg1FeGVjdXRlUmVzdWx0EhoKCHJlc3BvbnNlGAEgASgJUghyZXNwb25zZRIqCgZzdGF0dXMYAi'
+ 'ABKA4yEi5FeGVjdXRlU3RhdHVzVHlwZVIGc3RhdHVz');
diff --git a/lib/grpc/voice_agent_client.dart b/lib/grpc/voice_agent_client.dart
new file mode 100644
index 0000000..f25f0d2
--- /dev/null
+++ b/lib/grpc/voice_agent_client.dart
@@ -0,0 +1,71 @@
+import 'dart:async';
+import 'package:grpc/grpc.dart';
+import './generated/voice_agent.pbgrpc.dart';
+
+class VoiceAgentClient {
+ late ClientChannel _channel;
+ late VoiceAgentServiceClient _client;
+
+ VoiceAgentClient(String host, int port) {
+ // Initialize the client channel without connecting immediately
+ _channel = ClientChannel(
+ host,
+ port: port,
+ options: ChannelOptions(
+ credentials: ChannelCredentials.insecure(),
+ ),
+ );
+
+ _client = VoiceAgentServiceClient(_channel);
+ }
+
+ Future<ServiceStatus> checkServiceStatus() async {
+ final empty = Empty();
+ try {
+ final response = await _client.checkServiceStatus(empty);
+ return response;
+ } catch (e) {
+ print('Error calling CheckServiceStatus: $e');
+ // Handle the error gracefully, such as returning an error status
+ return ServiceStatus()..status = false;
+ }
+ }
+
+ Stream<WakeWordStatus> detectWakeWord() {
+ final empty = Empty();
+ try {
+ return _client.detectWakeWord(empty);
+ } catch (e) {
+ print('Error calling DetectWakeWord: $e');
+ // Handle the error gracefully, such as returning a default status
+ return Stream.empty(); // An empty stream as a placeholder
+ }
+ }
+
+ Future<RecognizeResult> recognizeVoiceCommand(
+ Stream<RecognizeControl> controlStream) async {
+ try {
+ final response = await _client.recognizeVoiceCommand(controlStream);
+ return response;
+ } catch (e) {
+ print('Error calling RecognizeVoiceCommand: $e');
+ // Handle the error gracefully, such as returning a default RecognizeResult
+ return RecognizeResult()..status = RecognizeStatusType.REC_ERROR;
+ }
+ }
+
+ Future<ExecuteResult> executeVoiceCommand(ExecuteInput input) async {
+ try {
+ final response = await _client.executeVoiceCommand(input);
+ return response;
+ } catch (e) {
+ print('Error calling ExecuteVoiceCommand: $e');
+ // Handle the error gracefully, such as returning an error status
+ return ExecuteResult()..status = ExecuteStatusType.EXEC_ERROR;
+ }
+ }
+
+ Future<void> shutdown() async {
+ await _channel.shutdown();
+ }
+}
diff --git a/lib/main.dart b/lib/main.dart
new file mode 100644
index 0000000..bc5b84b
--- /dev/null
+++ b/lib/main.dart
@@ -0,0 +1,77 @@
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import 'models/app_state.dart';
+import 'screens/home_screen.dart';
+import 'screens/error_screen.dart';
+import 'providers/service_status.dart';
+import 'grpc/generated/voice_agent.pbgrpc.dart';
+import 'grpc/voice_agent_client.dart';
+import 'utils/app_config.dart';
+
+void main() async {
+ WidgetsFlutterBinding.ensureInitialized();
+
+ final serviceStatusProvider = ServiceStatusProvider();
+
+ // get config variables
+ final config = await AppConfig.loadFromAsset();
+
+ // Check the service status initially
+ final initialServiceStatus = await checkServiceStatus(config);
+ serviceStatusProvider.setServiceStatus(initialServiceStatus.status);
+
+ runApp(
+ MultiProvider(
+ providers: [
+ ChangeNotifierProvider(create: (_) => AppState()),
+ ChangeNotifierProvider.value(value: serviceStatusProvider),
+ ],
+ child: App(
+ config: config,
+ onRetry: () async {
+ final serviceStatus = await checkServiceStatus(
+ config); // Retry connecting to the server
+
+ if (serviceStatus.status) {
+ serviceStatusProvider
+ .setServiceStatus(true); // Update the service status
+ }
+ },
+ ),
+ ),
+ );
+}
+
+Future<ServiceStatus> checkServiceStatus(AppConfig config) async {
+ final client = VoiceAgentClient(config.grpcHost, config.grpcPort);
+ final serviceStatus = await client.checkServiceStatus();
+ client.shutdown();
+ return serviceStatus;
+}
+
+class App extends StatelessWidget {
+ final AppConfig config;
+ final VoidCallback onRetry;
+
+ const App({Key? key, required this.config, required this.onRetry});
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ title: 'AGL Voice Assistant',
+ theme: ThemeData(
+ useMaterial3: true,
+ colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
+ ),
+ home: Consumer<ServiceStatusProvider>(
+ builder: (context, provider, child) {
+ return provider.isServiceOnline
+ ? HomePage(config: config)
+ : ErrorScreen(
+ onRetry: onRetry, // Pass the callback to the ErrorScreen
+ ); // Conditionally render HomePage or ErrorScreen
+ },
+ ),
+ );
+ }
+}
diff --git a/lib/models/app_state.dart b/lib/models/app_state.dart
new file mode 100644
index 0000000..be39bd2
--- /dev/null
+++ b/lib/models/app_state.dart
@@ -0,0 +1,8 @@
+import 'package:flutter/material.dart';
+
+class AppState extends ChangeNotifier {
+ bool isWakeWordDetected = false;
+ bool isWakeWordMode = false;
+ String intentEngine = "snips";
+ String streamId = "";
+}
diff --git a/lib/protos/voice_agent.proto b/lib/protos/voice_agent.proto
new file mode 100644
index 0000000..8c3ab65
--- /dev/null
+++ b/lib/protos/voice_agent.proto
@@ -0,0 +1,83 @@
+syntax = "proto3";
+
+
+service VoiceAgentService {
+ rpc CheckServiceStatus(Empty) returns (ServiceStatus);
+ rpc DetectWakeWord(Empty) returns (stream WakeWordStatus);
+ rpc RecognizeVoiceCommand(stream RecognizeControl) returns (RecognizeResult);
+ rpc ExecuteVoiceCommand(ExecuteInput) returns (ExecuteResult);
+}
+
+
+enum RecordAction {
+ START = 0;
+ STOP = 1;
+}
+
+enum NLUModel {
+ SNIPS = 0;
+ RASA = 1;
+}
+
+enum RecordMode {
+ MANUAL = 0;
+ AUTO = 1;
+}
+
+enum RecognizeStatusType {
+ REC_ERROR = 0;
+ REC_SUCCESS = 1;
+ REC_PROCESSING = 2;
+ VOICE_NOT_RECOGNIZED = 3;
+ INTENT_NOT_RECOGNIZED = 4;
+}
+
+enum ExecuteStatusType {
+ EXEC_ERROR = 0;
+ EXEC_SUCCESS = 1;
+ KUKSA_CONN_ERROR = 2;
+ INTENT_NOT_SUPPORTED = 3;
+ INTENT_SLOTS_INCOMPLETE = 4;
+}
+
+
+message Empty {}
+
+message ServiceStatus {
+ string version = 1;
+ bool status = 2;
+}
+
+message WakeWordStatus {
+ bool status = 1;
+}
+
+message RecognizeControl {
+ RecordAction action = 1;
+ NLUModel nlu_model = 2;
+ RecordMode record_mode = 3;
+ string stream_id = 4;
+}
+
+message IntentSlot {
+ string name = 1;
+ string value = 2;
+}
+
+message RecognizeResult {
+ string command = 1;
+ string intent = 2;
+ repeated IntentSlot intent_slots = 3;
+ string stream_id = 4;
+ RecognizeStatusType status = 5;
+}
+
+message ExecuteInput {
+ string intent = 1;
+ repeated IntentSlot intent_slots = 2;
+}
+
+message ExecuteResult {
+ string response = 1;
+ ExecuteStatusType status = 2;
+}
diff --git a/lib/providers/service_status.dart b/lib/providers/service_status.dart
new file mode 100644
index 0000000..595c92e
--- /dev/null
+++ b/lib/providers/service_status.dart
@@ -0,0 +1,12 @@
+import 'package:flutter/material.dart';
+
+class ServiceStatusProvider extends ChangeNotifier {
+ bool _isServiceOnline = false;
+
+ bool get isServiceOnline => _isServiceOnline;
+
+ void setServiceStatus(bool isOnline) {
+ _isServiceOnline = isOnline;
+ notifyListeners(); // Notify listeners (i.e., widgets that depend on this value) about the change
+ }
+}
diff --git a/lib/screens/error_screen.dart b/lib/screens/error_screen.dart
new file mode 100644
index 0000000..04a5d30
--- /dev/null
+++ b/lib/screens/error_screen.dart
@@ -0,0 +1,62 @@
+import 'package:flutter/material.dart';
+
+class ErrorScreen extends StatelessWidget {
+ final VoidCallback onRetry;
+
+ ErrorScreen({required this.onRetry});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: Center(
+ child: SizedBox(
+ width: MediaQuery.of(context).size.width * 0.7, // 70% of screen width
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ Icons.error_outline, // Use a Material Icon
+ size: 100,
+ color: Colors.red,
+ ),
+ SizedBox(height: 20), // Add some spacing
+ Text(
+ 'Oops!',
+ style: TextStyle(
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ color: Colors.red,
+ ),
+ ),
+ SizedBox(height: 10),
+ Text(
+ 'Unable to connect to the voice assistant backend. Make sure the "agl-voiceagent-service" is up and running in server mode with correct config values.',
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ fontSize: 18,
+ color: Colors.grey[700],
+ ),
+ ),
+ SizedBox(height: 20),
+ ElevatedButton(
+ onPressed: () {
+ onRetry(); // Call the retry callback
+ },
+ style: ElevatedButton.styleFrom(
+ foregroundColor: Colors.white,
+ backgroundColor: Colors.blue, // Set button color
+ ),
+ child: Text(
+ 'Retry',
+ style: TextStyle(
+ fontSize: 18,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart
new file mode 100644
index 0000000..d19d461
--- /dev/null
+++ b/lib/screens/home_screen.dart
@@ -0,0 +1,398 @@
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import 'dart:async';
+import '../models/app_state.dart';
+import '../widgets/nlu_engine_choice.dart';
+import '../widgets/assistant_mode_choice.dart';
+import '../widgets/record_command_button.dart';
+import '../widgets/listen_wake_word_section.dart';
+import '../widgets/chat_section.dart';
+import '../grpc/generated/voice_agent.pbgrpc.dart';
+import '../grpc/voice_agent_client.dart';
+import '../utils/app_config.dart';
+
+class HomePage extends StatefulWidget {
+ final AppConfig config;
+
+ HomePage({Key? key, required this.config});
+ @override
+ HomePageState createState() => HomePageState();
+}
+
+class HomePageState extends State<HomePage> {
+ late AppConfig _config; // Store the config as an instance variable
+ final ScrollController _scrollController = ScrollController();
+ List<ChatMessage> chatMessages = [];
+ StreamSubscription<WakeWordStatus>? _wakeWordStatusSubscription;
+ late VoiceAgentClient voiceAgentClient;
+
+ @override
+ void initState() {
+ super.initState();
+ _config = widget.config; // Initialize _config in the initState
+ addChatMessage(
+ "Assistant in Manual mode. You can send commands directly by pressing the record button.");
+ }
+
+ void changeAssistantMode(BuildContext context, AssistantMode newMode) {
+ final appState = context.read<AppState>();
+ clearChatMessages();
+ appState.streamId = "";
+ appState.isWakeWordDetected = false;
+
+ if (newMode == AssistantMode.wakeWord) {
+ appState.isWakeWordMode = true;
+ addChatMessage(
+ 'Switched to Wake Word mode. I\'ll listen for the wake word before responding.');
+ toggleWakeWordDetection(context, true);
+ } else if (newMode == AssistantMode.manual) {
+ appState.isWakeWordMode = false;
+ addChatMessage(
+ 'Switched to Manual mode. You can send commands directly by pressing record button.');
+ toggleWakeWordDetection(context, false);
+ }
+ print(appState.isWakeWordMode);
+ setState(() {}); // Trigger a rebuild
+ }
+
+ void changeIntentEngine(BuildContext context, NLUEngine newEngine) {
+ final appState = context.read<AppState>();
+
+ if (newEngine == NLUEngine.snips) {
+ appState.intentEngine = "snips";
+ addChatMessage(
+ 'Switched to 🚀 Snips engine. Lets be precise and accurate.');
+ } else if (newEngine == NLUEngine.rasa) {
+ appState.intentEngine = "rasa";
+ addChatMessage(
+ 'Switched to 🤖 RASA engine. Conversations just got smarter!');
+ }
+ print(appState.intentEngine);
+ setState(() {}); // Trigger a rebuild
+ }
+
+ void addChatMessage(String text, {bool isUserMessage = false}) {
+ final newMessage = ChatMessage(text: text, isUserMessage: isUserMessage);
+ setState(() {
+ chatMessages.add(newMessage);
+ });
+ // Use a post-frame callback to scroll after the frame has been rendered
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ _scrollController.animateTo(
+ _scrollController.position.maxScrollExtent,
+ duration: Duration(milliseconds: 300),
+ curve: Curves.easeInOut,
+ );
+ });
+ }
+
+ // Function to clear all chat messages
+ void clearChatMessages() {
+ setState(() {
+ chatMessages.clear();
+ });
+ }
+
+ void changeCommandRecordingState(
+ BuildContext context, bool isRecording) async {
+ final appState = context.read<AppState>();
+ if (isRecording) {
+ appState.streamId = await startRecording();
+ } else {
+ final response =
+ await stopRecording(appState.streamId, appState.intentEngine);
+ // Process and store the result
+ if (response.status == RecognizeStatusType.REC_SUCCESS) {
+ await executeVoiceCommand(
+ response); // Call executeVoiceCommand with the response
+ }
+ }
+ }
+
+ // gRPC related methods are as follows
+ // Function to start and stop the wake word detection loop
+ void toggleWakeWordDetection(BuildContext context, bool startDetection) {
+ final appState = context.read<AppState>();
+ if (startDetection) {
+ appState.isWakeWordDetected = false;
+ _startWakeWordDetection(context);
+ } else {
+ _stopWakeWordDetection();
+ }
+ }
+
+ // Function to start listening for wake word status responses
+ void _startWakeWordDetection(BuildContext context) {
+ final appState = context.read<AppState>();
+ voiceAgentClient = VoiceAgentClient(_config.grpcHost, _config.grpcPort);
+ _wakeWordStatusSubscription = voiceAgentClient.detectWakeWord().listen(
+ (response) {
+ if (response.status) {
+ // Wake word detected, you can handle this case here
+ // Set _isDetectingWakeWord to false to stop the loop
+ _stopWakeWordDetection();
+ appState.isWakeWordDetected = true;
+ addChatMessage(
+ 'Wake word detected! Now you can send your command by pressing the record button.');
+ setState(() {}); // Trigger a rebuild
+ }
+ },
+ onError: (error) {
+ // Handle any errors that occur during wake word detection
+ print('Error during wake word detection: $error');
+ // Set _isDetectingWakeWord to false to stop the loop
+ _stopWakeWordDetection();
+ },
+ cancelOnError: true,
+ );
+ }
+
+ // Function to stop listening for wake word status responses
+ void _stopWakeWordDetection() {
+ _wakeWordStatusSubscription?.cancel();
+ voiceAgentClient.shutdown();
+ }
+
+ Future<String> startRecording() async {
+ String streamId = "";
+ voiceAgentClient = VoiceAgentClient(_config.grpcHost, _config.grpcPort);
+ try {
+ // Create a RecognizeControl message to start recording
+ final controlMessage = RecognizeControl()
+ ..action = RecordAction.START
+ ..recordMode = RecordMode
+ .MANUAL; // You can change this to your desired record mode
+
+ // Create a Stream with the control message
+ final controlStream = Stream.fromIterable([controlMessage]);
+
+ // Call the gRPC method to start recording
+ final response =
+ await voiceAgentClient.recognizeVoiceCommand(controlStream);
+
+ streamId = response.streamId;
+ } catch (e) {
+ print('Error starting recording: $e');
+ addChatMessage('Recording failed. Please try again.');
+ }
+ return streamId;
+ }
+
+ Future<RecognizeResult> stopRecording(
+ String streamId, String nluModel) async {
+ try {
+ NLUModel model = NLUModel.RASA;
+ if (nluModel == "snips") {
+ model = NLUModel.SNIPS;
+ }
+ // Create a RecognizeControl message to stop recording
+ final controlMessage = RecognizeControl()
+ ..action = RecordAction.STOP
+ ..nluModel = model
+ ..streamId =
+ streamId // Use the same stream ID as when starting recording
+ ..recordMode = RecordMode.MANUAL;
+
+ // Create a Stream with the control message
+ final controlStream = Stream.fromIterable([controlMessage]);
+
+ // Call the gRPC method to stop recording
+ final response =
+ await voiceAgentClient.recognizeVoiceCommand(controlStream);
+
+ // Process and store the result
+ if (response.status == RecognizeStatusType.REC_SUCCESS) {
+ final command = response.command;
+ // final intent = response.intent;
+ // final intentSlots = response.intentSlots;
+ addChatMessage(command, isUserMessage: true);
+ } else if (response.status == RecognizeStatusType.INTENT_NOT_RECOGNIZED) {
+ final command = response.command;
+ addChatMessage(command, isUserMessage: true);
+ addChatMessage(
+ "Unable to undertsand the intent behind your request. Please try again.");
+ } else {
+ addChatMessage(
+ 'Failed to process your voice command. Please try again.');
+ }
+ await voiceAgentClient.shutdown();
+ return response;
+ } catch (e) {
+ print('Error stopping recording: $e');
+ addChatMessage('Failed to process your voice command. Please try again.');
+ await voiceAgentClient.shutdown();
+ return RecognizeResult()..status = RecognizeStatusType.REC_ERROR;
+ }
+ // await voiceAgentClient.shutdown();
+ }
+
+ Future<void> executeVoiceCommand(RecognizeResult response) async {
+ voiceAgentClient = VoiceAgentClient(_config.grpcHost, _config.grpcPort);
+ try {
+ // Create an ExecuteInput message using the response from stopRecording
+ final executeInput = ExecuteInput()
+ ..intent = response.intent
+ ..intentSlots.addAll(response.intentSlots);
+
+ // Call the gRPC method to execute the voice command
+ final execResponse =
+ await voiceAgentClient.executeVoiceCommand(executeInput);
+
+ // Handle the response as needed
+ if (execResponse.status == ExecuteStatusType.EXEC_SUCCESS) {
+ final commandResponse = execResponse.response;
+ addChatMessage(commandResponse);
+ } else if (execResponse.status == ExecuteStatusType.KUKSA_CONN_ERROR) {
+ final commandResponse = execResponse.response;
+ addChatMessage(commandResponse);
+ } else {
+ // Handle the case when execution fails
+ addChatMessage(
+ 'Failed to execute your voice command. Please try again.');
+ }
+ } catch (e) {
+ print('Error executing voice command: $e');
+ // Handle any errors that occur during the gRPC call
+ addChatMessage('Failed to execute your voice command. Please try again.');
+ }
+ await voiceAgentClient.shutdown();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final appState = context.watch<AppState>();
+
+ return Scaffold(
+ body: SingleChildScrollView(
+ child: Center(
+ child: SizedBox(
+ width:
+ MediaQuery.of(context).size.width * 0.85, // 85% of screen width
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.start,
+ children: [
+ Image.asset(
+ 'assets/agl_logo.png', // Replace with your logo image path
+ width: 120, // Adjust the width as needed
+ height: 120, // Adjust the height as needed
+ ),
+ Text(
+ "AGL Voice Assistant",
+ style: TextStyle(fontSize: 26, fontWeight: FontWeight.bold),
+ ),
+ SizedBox(height: 30),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Flexible(
+ flex: 1,
+ child: Card(
+ elevation: 4, // Add elevation for shadow
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Padding(
+ padding: EdgeInsets.all(16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Assistant Mode',
+ style: TextStyle(
+ fontSize: 20,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ SizedBox(height: 16), // Add spacing if needed
+ Center(
+ child: Consumer<AppState>(
+ builder: (context, appState, _) {
+ return AssistantModeChoice(
+ onModeChanged: (newMode) {
+ changeAssistantMode(context, newMode);
+ print(newMode);
+ },
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+
+ SizedBox(width: 20), // Add spacing between buttons
+
+ Flexible(
+ flex: 1,
+ child: Card(
+ elevation: 4, // Add elevation for shadow
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Padding(
+ padding: EdgeInsets.all(16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Intent Engine',
+ style: TextStyle(
+ fontSize: 20,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ SizedBox(height: 16), // Add spacing if needed
+ Center(
+ child: Consumer<AppState>(
+ builder: (context, appState, _) {
+ return NLUEngineChoice(
+ onEngineChanged: (newEngine) {
+ changeIntentEngine(context, newEngine);
+ print(newEngine);
+ },
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ SizedBox(height: 30),
+ ChatSection(
+ scrollController: _scrollController,
+ chatMessages: chatMessages,
+ addChatMessage: addChatMessage,
+ ),
+ SizedBox(height: 30),
+ if (!appState.isWakeWordMode || appState.isWakeWordDetected)
+ Center(
+ child: Consumer<AppState>(builder: (context, appState, _) {
+ return RecordCommandButton(
+ onRecordingStateChanged: (isRecording) {
+ changeCommandRecordingState(context, isRecording);
+ },
+ );
+ }),
+ )
+ else
+ Center(
+ child: Consumer<AppState>(
+ builder: (context, appState, _) {
+ return ListeningForWakeWordSection();
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/utils/app_config.dart b/lib/utils/app_config.dart
new file mode 100644
index 0000000..8f5c566
--- /dev/null
+++ b/lib/utils/app_config.dart
@@ -0,0 +1,26 @@
+import 'dart:convert';
+import 'dart:async';
+import 'package:flutter/services.dart';
+
+class AppConfig {
+ late String grpcHost;
+ late int grpcPort;
+
+ AppConfig({required this.grpcHost, required this.grpcPort});
+
+ factory AppConfig.fromAsset() {
+ return AppConfig._();
+ }
+
+ AppConfig._();
+
+ static Future<AppConfig> loadFromAsset() async {
+ final configString = await rootBundle.loadString('assets/config.json');
+ final jsonMap = json.decode(configString);
+
+ return AppConfig(
+ grpcHost: jsonMap['grpc_host'],
+ grpcPort: jsonMap['grpc_port'],
+ );
+ }
+}
diff --git a/lib/widgets/assistant_mode_choice.dart b/lib/widgets/assistant_mode_choice.dart
new file mode 100644
index 0000000..ec17534
--- /dev/null
+++ b/lib/widgets/assistant_mode_choice.dart
@@ -0,0 +1,202 @@
+// import 'package:flutter/material.dart';
+// import 'package:provider/provider.dart';
+// import '../models/app_state.dart';
+
+// enum AssistantMode { wakeWord, manual }
+
+// class AssistantModeChoice extends StatefulWidget {
+// final Function(AssistantMode) onModeChanged;
+
+// const AssistantModeChoice({Key? key, required this.onModeChanged})
+// : super(key: key);
+
+// @override
+// AssistantModeChoiceState createState() => AssistantModeChoiceState();
+// }
+
+// class AssistantModeChoiceState extends State<AssistantModeChoice> {
+// @override
+// Widget build(BuildContext context) {
+// final appState = context.watch<AppState>(); // Watch the app state
+
+// return SegmentedButton<AssistantMode>(
+// segments: const <ButtonSegment<AssistantMode>>[
+// ButtonSegment<AssistantMode>(
+// value: AssistantMode.wakeWord,
+// label: Text('Wake Word'),
+// icon: Icon(Icons.graphic_eq)),
+// ButtonSegment<AssistantMode>(
+// value: AssistantMode.manual,
+// label: Text('Manual'),
+// icon: Icon(Icons.graphic_eq)),
+// ],
+// selected: <AssistantMode>{
+// appState.isWakeWordMode ? AssistantMode.wakeWord : AssistantMode.manual
+// }, // Use app state
+// onSelectionChanged: (Set<AssistantMode> newSelection) {
+// final newMode = newSelection.first;
+// setState(() {
+// // Update the app state when the mode changes
+// appState.isWakeWordMode = newMode == AssistantMode.wakeWord;
+// });
+// // Call the callback function to notify the mode change
+// widget.onModeChanged(newMode);
+// },
+// style: ButtonStyle(
+// side: MaterialStateProperty.all<BorderSide>(
+// BorderSide(
+// width: 0, // Remove border width
+// color: Colors.transparent, // Make border transparent
+// ),
+// ),
+// backgroundColor: MaterialStateProperty.resolveWith<Color>(
+// (states) {
+// if (states.contains(MaterialState.selected)) {
+// return Colors.green; // Color when pressed
+// }
+// // Add more conditions for other states as needed
+// return Colors.white; // Default color
+// },
+// ),
+// foregroundColor: MaterialStateProperty.resolveWith<Color>((states) {
+// if (states.contains(MaterialState.selected)) {
+// return Colors.white; // Color when pressed
+// }
+// // Add more conditions for other states as needed
+// return Colors.green;
+// })),
+// );
+// }
+// }
+
+import 'package:flutter/material.dart';
+
+enum AssistantMode { wakeWord, manual }
+
+class AssistantModeChoice extends StatefulWidget {
+ final Function(AssistantMode) onModeChanged;
+
+ const AssistantModeChoice({Key? key, required this.onModeChanged})
+ : super(key: key);
+
+ @override
+ AssistantModeChoiceState createState() => AssistantModeChoiceState();
+}
+
+class AssistantModeChoiceState extends State<AssistantModeChoice> {
+ late AssistantMode _selectedMode;
+
+ @override
+ void initState() {
+ super.initState();
+ _selectedMode = AssistantMode.manual; // Initialize the selection
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: <Widget>[
+ InkWell(
+ onTap: () => _onModeChanged(AssistantMode.wakeWord),
+ borderRadius: BorderRadius.only(
+ topLeft: Radius.circular(20.0),
+ bottomLeft: Radius.circular(20.0),
+ ),
+ child: Container(
+ padding: EdgeInsets.symmetric(horizontal: 17.5, vertical: 5.0),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.only(
+ topLeft: Radius.circular(20.0),
+ bottomLeft: Radius.circular(20.0),
+ ),
+ color: _selectedMode == AssistantMode.wakeWord
+ ? Colors.green
+ : Colors.white,
+ border: Border.all(
+ color: Colors.transparent,
+ ),
+ ),
+ child: Row(
+ children: [
+ Icon(
+ _selectedMode == AssistantMode.wakeWord
+ ? Icons.check
+ : Icons.graphic_eq,
+ color: _selectedMode == AssistantMode.wakeWord
+ ? Colors.white
+ : Colors.green,
+ ),
+ SizedBox(width: 8),
+ Text(
+ 'Wake Word',
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 18,
+ color: _selectedMode == AssistantMode.wakeWord
+ ? Colors.white
+ : Colors.green,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ InkWell(
+ onTap: () => _onModeChanged(AssistantMode.manual),
+ borderRadius: BorderRadius.only(
+ topRight: Radius.circular(20.0),
+ bottomRight: Radius.circular(20.0),
+ ),
+ child: Container(
+ padding: EdgeInsets.symmetric(horizontal: 17.5, vertical: 5.0),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.only(
+ topRight: Radius.circular(20.0),
+ bottomRight: Radius.circular(20.0),
+ ),
+ color: _selectedMode == AssistantMode.manual
+ ? Colors.green
+ : Colors.white,
+ border: Border.all(
+ color: Colors.transparent,
+ ),
+ ),
+ child: Row(
+ children: [
+ Icon(
+ _selectedMode == AssistantMode.manual
+ ? Icons.check
+ : Icons.graphic_eq,
+ color: _selectedMode == AssistantMode.manual
+ ? Colors.white
+ : Colors.green,
+ ),
+ SizedBox(width: 8),
+ Text(
+ 'Manual',
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 18,
+ color: _selectedMode == AssistantMode.manual
+ ? Colors.white
+ : Colors.green,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+
+ void _onModeChanged(AssistantMode newMode) {
+ setState(() {
+ _selectedMode = newMode;
+ });
+
+ // Call the callback function to notify the mode change
+ widget.onModeChanged(newMode);
+ }
+}
diff --git a/lib/widgets/chat_section.dart b/lib/widgets/chat_section.dart
new file mode 100644
index 0000000..596b9f3
--- /dev/null
+++ b/lib/widgets/chat_section.dart
@@ -0,0 +1,128 @@
+import 'package:flutter/material.dart';
+
+class ChatSection extends StatelessWidget {
+ final ScrollController scrollController;
+ final List<ChatMessage> chatMessages;
+ final Function(String text, {bool isUserMessage}) addChatMessage;
+
+ ChatSection({
+ required this.scrollController,
+ required this.chatMessages,
+ required this.addChatMessage,
+ });
+
+ @override
+ @override
+ Widget build(BuildContext context) {
+ return Card(
+ elevation: 4, // Add a subtle shadow
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Column(
+ children: [
+ // Chat heading
+ Container(
+ padding: EdgeInsets.all(6),
+ alignment: Alignment.center,
+ child: Text(
+ 'Command Logs',
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 20,
+ ),
+ ),
+ ),
+ // Chat messages with fixed height
+ Container(
+ padding: EdgeInsets.all(12),
+ height: 180, // Adjust the height as needed
+ child: ListView.builder(
+ controller: scrollController,
+ itemCount: chatMessages.length,
+ itemBuilder: (context, index) {
+ final message = chatMessages[index];
+ return ChatMessageTile(message: message);
+ },
+ ),
+ ),
+ // User input field (if needed)
+ // ...
+ ],
+ ),
+ );
+ }
+}
+
+class ChatMessage {
+ final String text;
+ final bool isUserMessage;
+
+ ChatMessage({required this.text, this.isUserMessage = false});
+}
+
+class ChatMessageTile extends StatelessWidget {
+ final ChatMessage message;
+
+ ChatMessageTile({required this.message});
+
+ @override
+ Widget build(BuildContext context) {
+ return ListTile(
+ contentPadding: EdgeInsets.all(0),
+ title: Container(
+ alignment:
+ message.isUserMessage ? Alignment.topRight : Alignment.topLeft,
+ child: Row(
+ mainAxisAlignment: message.isUserMessage
+ ? MainAxisAlignment.end
+ : MainAxisAlignment.start,
+ children: [
+ if (!message.isUserMessage)
+ CircleAvatar(
+ backgroundColor: Colors.green[400],
+ child: Icon(
+ Icons.smart_toy_outlined,
+ color: Colors.white,
+ ),
+ ),
+ SizedBox(width: 8),
+ Flexible(
+ child: Container(
+ padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.only(
+ topLeft: Radius.circular(16),
+ topRight: Radius.circular(16),
+ bottomLeft: message.isUserMessage
+ ? Radius.circular(16)
+ : Radius.circular(0),
+ bottomRight: message.isUserMessage
+ ? Radius.circular(0)
+ : Radius.circular(16),
+ ),
+ color:
+ message.isUserMessage ? Colors.blue : Colors.green[400],
+ ),
+ child: Text(
+ message.text,
+ style: TextStyle(color: Colors.white, fontSize: 18),
+ maxLines: null,
+ ),
+ ),
+ ),
+ SizedBox(width: 8),
+ if (message.isUserMessage)
+ CircleAvatar(
+ backgroundColor: Colors.blue,
+ child: Icon(
+ Icons.person,
+ color: Colors.white,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/widgets/listen_wake_word_section.dart b/lib/widgets/listen_wake_word_section.dart
new file mode 100644
index 0000000..61abcd0
--- /dev/null
+++ b/lib/widgets/listen_wake_word_section.dart
@@ -0,0 +1,66 @@
+import 'package:flutter/material.dart';
+
+class ListeningForWakeWordSection extends StatefulWidget {
+ @override
+ ListeningForWakeWordSectionState createState() =>
+ ListeningForWakeWordSectionState();
+}
+
+class ListeningForWakeWordSectionState
+ extends State<ListeningForWakeWordSection>
+ with SingleTickerProviderStateMixin {
+ late AnimationController _controller;
+ late Animation<Color?> _colorAnimation;
+
+ @override
+ void initState() {
+ super.initState();
+
+ // Create an animation controller
+ _controller = AnimationController(
+ vsync: this,
+ duration: Duration(milliseconds: 1500), // Adjust the duration as needed
+ );
+
+ // Create a color change animation
+ _colorAnimation = ColorTween(
+ begin: Colors.orangeAccent, // Use your desired initial color
+ end: Colors.redAccent, // Use your desired final color
+ ).animate(_controller);
+
+ // Start both animations
+ _controller.repeat(reverse: true);
+ }
+
+ @override
+ void dispose() {
+ _controller.dispose(); // Dispose of the animation controller
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: [
+ AnimatedBuilder(
+ animation: _controller,
+ builder: (context, child) {
+ return Icon(
+ Icons.album, // Replace with your listening icon
+ size: 60,
+ color: _colorAnimation.value,
+ );
+ },
+ ),
+ SizedBox(height: 8),
+ Text(
+ 'Listening for wake word...',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/widgets/nlu_engine_choice.dart b/lib/widgets/nlu_engine_choice.dart
new file mode 100644
index 0000000..1e8ca52
--- /dev/null
+++ b/lib/widgets/nlu_engine_choice.dart
@@ -0,0 +1,200 @@
+// import 'package:flutter/material.dart';
+
+// enum NLUEngine { snips, rasa }
+
+// class NLUEngineChoice extends StatefulWidget {
+// final Function(NLUEngine) onEngineChanged;
+
+// const NLUEngineChoice({Key? key, required this.onEngineChanged})
+// : super(key: key);
+
+// @override
+// State<NLUEngineChoice> createState() => _NLUEngineChoiceState();
+// }
+
+// class _NLUEngineChoiceState extends State<NLUEngineChoice> {
+// NLUEngine nluView = NLUEngine.snips;
+
+// @override
+// Widget build(BuildContext context) {
+// return SegmentedButton<NLUEngine>(
+// segments: const <ButtonSegment<NLUEngine>>[
+// ButtonSegment<NLUEngine>(
+// value: NLUEngine.snips,
+// label: Text('Snips'),
+// icon: Icon(Icons.settings_suggest)),
+// ButtonSegment<NLUEngine>(
+// value: NLUEngine.rasa,
+// label: Text('RASA'),
+// icon: Icon(Icons.settings_suggest)),
+// ],
+// selected: <NLUEngine>{nluView},
+// onSelectionChanged: (Set<NLUEngine> newSelection) {
+// final newEngine = newSelection.first;
+// setState(() {
+// // By default there is only a single segment that can be
+// // selected at one time, so its value is always the first
+// // item in the selected set.
+// nluView = newEngine;
+// });
+// // Call the callback function to notify the mode change
+// widget.onEngineChanged(newEngine);
+// },
+// style: ButtonStyle(
+// side: MaterialStateProperty.all<BorderSide>(
+// BorderSide(
+// width: 0, // Remove border width
+// color: Colors.transparent, // Make border transparent
+// ),
+// ),
+// backgroundColor: MaterialStateProperty.resolveWith<Color>(
+// (states) {
+// if (states.contains(MaterialState.selected)) {
+// return Colors.green; // Color when pressed
+// }
+// // Add more conditions for other states as needed
+// return Colors.white; // Default color
+// },
+// ),
+// foregroundColor: MaterialStateProperty.resolveWith<Color>((states) {
+// if (states.contains(MaterialState.selected)) {
+// return Colors.white; // Color when pressed
+// }
+// // Add more conditions for other states as needed
+// return Colors.green;
+// })),
+// );
+// }
+// }
+
+import 'package:flutter/material.dart';
+
+enum NLUEngine { snips, rasa }
+
+class NLUEngineChoice extends StatefulWidget {
+ final Function(NLUEngine) onEngineChanged;
+
+ const NLUEngineChoice({Key? key, required this.onEngineChanged})
+ : super(key: key);
+
+ @override
+ State<NLUEngineChoice> createState() => _NLUEngineChoiceState();
+}
+
+class _NLUEngineChoiceState extends State<NLUEngineChoice> {
+ late NLUEngine _selectedEngine;
+
+ @override
+ void initState() {
+ super.initState();
+ _selectedEngine = NLUEngine.snips; // Initialize the selection
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: <Widget>[
+ InkWell(
+ onTap: () => _onEngineChanged(NLUEngine.snips),
+ borderRadius: BorderRadius.only(
+ topLeft: Radius.circular(20.0),
+ bottomLeft: Radius.circular(20.0),
+ ),
+ child: Container(
+ padding: EdgeInsets.symmetric(horizontal: 17.5, vertical: 5.0),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.only(
+ topLeft: Radius.circular(20.0),
+ bottomLeft: Radius.circular(20.0),
+ ),
+ color: _selectedEngine == NLUEngine.snips
+ ? Colors.green
+ : Colors.white,
+ border: Border.all(
+ color: Colors.transparent,
+ ),
+ ),
+ child: Row(
+ children: [
+ Icon(
+ _selectedEngine == NLUEngine.snips
+ ? Icons.check
+ : Icons.settings_suggest,
+ color: _selectedEngine == NLUEngine.snips
+ ? Colors.white
+ : Colors.green,
+ ),
+ SizedBox(width: 8),
+ Text(
+ 'Snips',
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 18,
+ color: _selectedEngine == NLUEngine.snips
+ ? Colors.white
+ : Colors.green,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ InkWell(
+ onTap: () => _onEngineChanged(NLUEngine.rasa),
+ borderRadius: BorderRadius.only(
+ topRight: Radius.circular(20.0),
+ bottomRight: Radius.circular(20.0),
+ ),
+ child: Container(
+ padding: EdgeInsets.symmetric(horizontal: 17.5, vertical: 5.0),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.only(
+ topRight: Radius.circular(20.0),
+ bottomRight: Radius.circular(20.0),
+ ),
+ color: _selectedEngine == NLUEngine.rasa
+ ? Colors.green
+ : Colors.white,
+ border: Border.all(
+ color: Colors.transparent,
+ ),
+ ),
+ child: Row(
+ children: [
+ Icon(
+ _selectedEngine == NLUEngine.rasa
+ ? Icons.check
+ : Icons.settings_suggest,
+ color: _selectedEngine == NLUEngine.rasa
+ ? Colors.white
+ : Colors.green,
+ ),
+ SizedBox(width: 8),
+ Text(
+ 'RASA',
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 18,
+ color: _selectedEngine == NLUEngine.rasa
+ ? Colors.white
+ : Colors.green,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+
+ void _onEngineChanged(NLUEngine newEngine) {
+ setState(() {
+ _selectedEngine = newEngine;
+ });
+
+ // Call the callback function to notify the engine change
+ widget.onEngineChanged(newEngine);
+ }
+}
diff --git a/lib/widgets/record_command_button.dart b/lib/widgets/record_command_button.dart
new file mode 100644
index 0000000..fdff772
--- /dev/null
+++ b/lib/widgets/record_command_button.dart
@@ -0,0 +1,66 @@
+import 'package:flutter/material.dart';
+
+class RecordCommandButton extends StatefulWidget {
+ final ValueChanged<bool> onRecordingStateChanged;
+
+ RecordCommandButton({required this.onRecordingStateChanged});
+
+ @override
+ RecordCommandButtonState createState() => RecordCommandButtonState();
+}
+
+class RecordCommandButtonState extends State<RecordCommandButton> {
+ bool isRecording = false;
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: [
+ AnimatedContainer(
+ duration: Duration(seconds: 1),
+ curve: Curves.easeInOut,
+ width: 60, // Adjust the button size as needed
+ height: 60, // Adjust the button size as needed
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ color: isRecording
+ ? Colors.red
+ : Colors.green, // Green when recording, red when not recording
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.5),
+ blurRadius: 5,
+ spreadRadius: 1,
+ ),
+ ],
+ ),
+ child: InkWell(
+ onTap: () {
+ // Toggle recording state
+ setState(() {
+ isRecording = !isRecording;
+ });
+ // Call the callback function with the recording state
+ widget.onRecordingStateChanged(isRecording);
+ },
+ child: Center(
+ child: Icon(
+ Icons.mic, // Microphone icon
+ size: 36, // Icon size
+ color: Colors.white, // Icon color
+ ),
+ ),
+ ),
+ ),
+ SizedBox(height: 8), // Add spacing between the button and text
+ Text(
+ isRecording ? 'Recording...' : 'Record Command',
+ style: TextStyle(
+ fontSize: 18, // Text size
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ );
+ }
+}