Merge pull request #166 from cogwheel0/android-assistant-trigger-customization

feat(profile): Add Android assistant trigger customization option
This commit is contained in:
cogwheel
2025-11-24 15:08:07 +05:30
committed by GitHub
16 changed files with 410 additions and 7 deletions

View File

@@ -10,10 +10,29 @@ import android.graphics.Bitmap
class ConduitVoiceInteractionSession(context: Context) : VoiceInteractionSession(context) {
companion object {
private const val PREFS_FILE = "FlutterSharedPreferences"
private const val TRIGGER_KEY = "flutter.android_assistant_trigger"
private const val TRIGGER_OVERLAY = "overlay"
private const val TRIGGER_NEW_CHAT = "new_chat"
private const val TRIGGER_VOICE_CALL = "voice_call"
}
private var capturedContext: String? = null
private var capturedScreenshot: Bitmap? = null
override fun onCreateContentView(): android.view.View {
when (getTriggerPreference()) {
TRIGGER_NEW_CHAT -> {
launchAppForNewChat()
return android.view.View(context)
}
TRIGGER_VOICE_CALL -> {
launchAppForVoiceCall()
return android.view.View(context)
}
}
val view = layoutInflater.inflate(app.cogwheel.conduit.R.layout.assistant_overlay, null)
// Summarize page button - sends screen context
@@ -157,6 +176,25 @@ class ConduitVoiceInteractionSession(context: Context) : VoiceInteractionSession
}
}
private fun launchAppForNewChat() {
try {
android.util.Log.d("ConduitVoiceSession", "Attempting to launch app for new chat")
val intent = Intent(context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
intent.putExtra("start_new_chat", true)
android.util.Log.d("ConduitVoiceSession", "New chat flag attached")
context.startActivity(intent)
android.util.Log.d("ConduitVoiceSession", "App launch requested for new chat")
finish()
} catch (e: Exception) {
android.util.Log.e("ConduitVoiceSession", "Failed to launch app for new chat", e)
}
}
private fun launchAppForVoiceCall() {
try {
android.util.Log.d("ConduitVoiceSession", "Attempting to launch app for voice call")
@@ -176,6 +214,15 @@ class ConduitVoiceInteractionSession(context: Context) : VoiceInteractionSession
}
}
private fun getTriggerPreference(): String {
return try {
val prefs = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE)
prefs.getString(TRIGGER_KEY, TRIGGER_OVERLAY) ?: TRIGGER_OVERLAY
} catch (e: Exception) {
TRIGGER_OVERLAY
}
}
private fun traverseNode(node: AssistStructure.ViewNode?, builder: StringBuilder) {
if (node == null) return

View File

@@ -51,15 +51,20 @@ class MainActivity : FlutterActivity() {
val screenContext = intent.getStringExtra("screen_context")
val screenshotPath = intent.getStringExtra("screenshot_path")
val startVoiceCall = intent.getBooleanExtra("start_voice_call", false)
val startNewChat = intent.getBooleanExtra("start_new_chat", false)
android.util.Log.d("MainActivity", "screenContext: $screenContext")
android.util.Log.d("MainActivity", "screenshotPath: $screenshotPath")
android.util.Log.d("MainActivity", "startVoiceCall: $startVoiceCall")
android.util.Log.d("MainActivity", "startNewChat: $startNewChat")
android.util.Log.d("MainActivity", "methodChannel: $methodChannel")
if (startVoiceCall) {
android.util.Log.d("MainActivity", "Invoking startVoiceCall")
methodChannel?.invokeMethod("startVoiceCall", null)
} else if (startNewChat) {
android.util.Log.d("MainActivity", "Invoking startNewChat")
methodChannel?.invokeMethod("startNewChat", null)
} else if (screenContext != null) {
android.util.Log.d("MainActivity", "Invoking analyzeScreen")
methodChannel?.invokeMethod("analyzeScreen", screenContext)

View File

@@ -29,6 +29,7 @@ final class PreferenceKeys {
static const String ttsServerVoiceId = 'tts_server_voice_id';
static const String ttsServerVoiceName = 'tts_server_voice_name';
static const String voiceSilenceDuration = 'voice_silence_duration';
static const String androidAssistantTrigger = 'android_assistant_trigger';
}
final class LegacyPreferenceKeys {

View File

@@ -3,6 +3,7 @@ import 'dart:developer' as developer;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:hive_ce/hive.dart';
import '../persistence/hive_bootstrap.dart';
import '../persistence/hive_boxes.dart';
@@ -17,6 +18,22 @@ enum SttPreference { deviceOnly, serverOnly }
/// TTS engine selection
enum TtsEngine { device, server }
/// Action to take when the Android digital assistant is triggered.
enum AndroidAssistantTrigger { overlay, newChat, voiceCall }
extension AndroidAssistantTriggerStorage on AndroidAssistantTrigger {
String get storageValue {
switch (this) {
case AndroidAssistantTrigger.overlay:
return 'overlay';
case AndroidAssistantTrigger.newChat:
return 'new_chat';
case AndroidAssistantTrigger.voiceCall:
return 'voice_call';
}
}
}
/// Service for managing app-wide settings including accessibility preferences
class SettingsService {
static const String _reduceMotionKey = PreferenceKeys.reduceMotion;
@@ -41,6 +58,8 @@ class SettingsService {
// Voice silence duration for auto-stop (milliseconds)
static const String _voiceSilenceDurationKey =
PreferenceKeys.voiceSilenceDuration;
static const String _androidAssistantTriggerKey =
PreferenceKeys.androidAssistantTrigger;
static Box<dynamic> _preferencesBox() =>
Hive.box<dynamic>(HiveBoxNames.preferences);
@@ -153,6 +172,8 @@ class SettingsService {
PreferenceKeys.ttsEngine: settings.ttsEngine.name,
PreferenceKeys.voiceSttPreference: settings.sttPreference.name,
_voiceSilenceDurationKey: settings.voiceSilenceDuration,
_androidAssistantTriggerKey:
settings.androidAssistantTrigger.storageValue,
};
await box.putAll(updates);
@@ -191,6 +212,8 @@ class SettingsService {
} else {
await box.delete(PreferenceKeys.ttsServerVoiceName);
}
await _writeAssistantTriggerToSharedPrefs(settings.androidAssistantTrigger);
}
static TtsEngine _parseTtsEngine(String? raw) {
@@ -219,6 +242,20 @@ class SettingsService {
}
}
static AndroidAssistantTrigger _parseAndroidAssistantTrigger(String? raw) {
switch ((raw ?? '').toLowerCase()) {
case 'new_chat':
case 'newchat':
return AndroidAssistantTrigger.newChat;
case 'voice_call':
case 'voicecall':
return AndroidAssistantTrigger.voiceCall;
case 'overlay':
default:
return AndroidAssistantTrigger.overlay;
}
}
// Voice input specific settings
static Future<String?> getVoiceLocaleId() {
final value = _preferencesBox().get(_voiceLocaleKey) as String?;
@@ -309,6 +346,30 @@ class SettingsService {
return _preferencesBox().put(_voiceSilenceDurationKey, sanitized);
}
static Future<void> setAndroidAssistantTrigger(
AndroidAssistantTrigger trigger,
) async {
await _preferencesBox().put(
_androidAssistantTriggerKey,
trigger.storageValue,
);
await _writeAssistantTriggerToSharedPrefs(trigger);
}
static Future<void> _writeAssistantTriggerToSharedPrefs(
AndroidAssistantTrigger trigger,
) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(
PreferenceKeys.androidAssistantTrigger,
trigger.storageValue,
);
} catch (_) {
// SharedPreferences writes are best-effort for Android assistant access
}
}
/// Get effective animation duration considering all settings
static Duration getEffectiveAnimationDuration(
BuildContext context,
@@ -372,6 +433,9 @@ class SettingsService {
sttPreference: _parseSttPreference(
box.get(PreferenceKeys.voiceSttPreference) as String?,
),
androidAssistantTrigger: _parseAndroidAssistantTrigger(
box.get(_androidAssistantTriggerKey) as String?,
),
voiceSilenceDuration: (box.get(_voiceSilenceDurationKey) as int? ?? 2000)
.clamp(300, 3000),
);
@@ -406,6 +470,7 @@ class AppSettings {
final TtsEngine ttsEngine;
final String? ttsServerVoiceId;
final String? ttsServerVoiceName;
final AndroidAssistantTrigger androidAssistantTrigger;
final int voiceSilenceDuration;
const AppSettings({
this.reduceMotion = false,
@@ -429,6 +494,7 @@ class AppSettings {
this.ttsEngine = TtsEngine.device,
this.ttsServerVoiceId,
this.ttsServerVoiceName,
this.androidAssistantTrigger = AndroidAssistantTrigger.overlay,
this.voiceSilenceDuration = 2000,
});
@@ -455,6 +521,7 @@ class AppSettings {
Object? ttsServerVoiceId = const _DefaultValue(),
Object? ttsServerVoiceName = const _DefaultValue(),
int? voiceSilenceDuration,
AndroidAssistantTrigger? androidAssistantTrigger,
}) {
return AppSettings(
reduceMotion: reduceMotion ?? this.reduceMotion,
@@ -486,6 +553,8 @@ class AppSettings {
ttsServerVoiceName: ttsServerVoiceName is _DefaultValue
? this.ttsServerVoiceName
: ttsServerVoiceName as String?,
androidAssistantTrigger:
androidAssistantTrigger ?? this.androidAssistantTrigger,
voiceSilenceDuration: voiceSilenceDuration ?? this.voiceSilenceDuration,
);
}
@@ -513,6 +582,7 @@ class AppSettings {
other.ttsEngine == ttsEngine &&
other.ttsServerVoiceId == ttsServerVoiceId &&
other.ttsServerVoiceName == ttsServerVoiceName &&
other.androidAssistantTrigger == androidAssistantTrigger &&
other.voiceSilenceDuration == voiceSilenceDuration &&
_listEquals(other.quickPills, quickPills);
// socketTransportMode intentionally not included in == to avoid frequent rebuilds
@@ -541,6 +611,7 @@ class AppSettings {
ttsEngine,
ttsServerVoiceId,
ttsServerVoiceName,
androidAssistantTrigger,
voiceSilenceDuration,
Object.hashAllUnordered(quickPills),
]);
@@ -715,6 +786,16 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
await SettingsService.setVoiceSilenceDuration(milliseconds);
}
Future<void> setAndroidAssistantTrigger(
AndroidAssistantTrigger trigger,
) async {
if (state.androidAssistantTrigger == trigger) {
return;
}
state = state.copyWith(androidAssistantTrigger: trigger);
await SettingsService.setAndroidAssistantTrigger(trigger);
}
Future<void> resetToDefaults() async {
const defaultSettings = AppSettings();
await SettingsService.saveSettings(defaultSettings);

View File

@@ -46,19 +46,27 @@ class AndroidAssistantHandler {
await _processScreenshot(screenshotPath);
} else if (call.method == 'startVoiceCall') {
await _startVoiceCall();
} else if (call.method == 'startNewChat') {
await _startNewChat();
}
}
Future<void> _processScreenshot(String screenshotPath) async {
try {
DebugLogger.log('Processing screenshot: $screenshotPath', scope: 'assistant');
DebugLogger.log(
'Processing screenshot: $screenshotPath',
scope: 'assistant',
);
// Wait for app to be ready (authenticated and model available)
final navState = _ref.read(authNavigationStateProvider);
final model = _ref.read(selectedModelProvider);
if (navState != AuthNavigationState.authenticated || model == null) {
DebugLogger.log('App not ready for screenshot processing', scope: 'assistant');
DebugLogger.log(
'App not ready for screenshot processing',
scope: 'assistant',
);
return;
}
@@ -75,7 +83,10 @@ class AndroidAssistantHandler {
// Add screenshot as attachment
final file = File(screenshotPath);
if (!await file.exists()) {
DebugLogger.log('Screenshot file not found: $screenshotPath', scope: 'assistant');
DebugLogger.log(
'Screenshot file not found: $screenshotPath',
scope: 'assistant',
);
return;
}
@@ -91,15 +102,23 @@ class AndroidAssistantHandler {
// Enqueue upload via task queue
final activeConv = _ref.read(activeConversationProvider);
try {
await _ref.read(taskQueueProvider.notifier).enqueueUploadMedia(
await _ref
.read(taskQueueProvider.notifier)
.enqueueUploadMedia(
conversationId: activeConv?.id,
filePath: attachment.file.path,
fileName: attachment.displayName,
fileSize: await attachment.file.length(),
);
DebugLogger.log('Screenshot uploaded successfully', scope: 'assistant');
DebugLogger.log(
'Screenshot uploaded successfully',
scope: 'assistant',
);
} catch (e) {
DebugLogger.log('Failed to upload screenshot: $e', scope: 'assistant');
DebugLogger.log(
'Failed to upload screenshot: $e',
scope: 'assistant',
);
}
}
} catch (e) {
@@ -130,7 +149,10 @@ class AndroidAssistantHandler {
// Get the current BuildContext from the navigation service
final context = NavigationService.navigatorKey.currentContext;
if (context == null) {
DebugLogger.log('No context available for voice call navigation', scope: 'assistant');
DebugLogger.log(
'No context available for voice call navigation',
scope: 'assistant',
);
return;
}
@@ -147,4 +169,28 @@ class AndroidAssistantHandler {
DebugLogger.log('Failed to start voice call: $e', scope: 'assistant');
}
}
Future<void> _startNewChat() async {
try {
DebugLogger.log('Starting new chat from assistant', scope: 'assistant');
final navState = _ref.read(authNavigationStateProvider);
final model = _ref.read(selectedModelProvider);
if (navState != AuthNavigationState.authenticated || model == null) {
DebugLogger.log('App not ready for new chat', scope: 'assistant');
return;
}
final isOnChatRoute = NavigationService.currentRoute == Routes.chat;
if (!isOnChatRoute) {
await NavigationService.navigateToChat();
}
startNewChat(_ref);
DebugLogger.log('New chat started from assistant', scope: 'assistant');
} catch (e) {
DebugLogger.log('Failed to start new chat: $e', scope: 'assistant');
}
}
}

View File

@@ -407,6 +407,10 @@ class AppCustomizationPage extends ConsumerWidget {
final transportLabel = activeTransportMode == 'polling'
? l10n.transportModePolling
: l10n.transportModeWs;
final assistantTriggerLabel = _androidAssistantTriggerLabel(
l10n,
settings.androidAssistantTrigger,
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -467,10 +471,164 @@ class AppCustomizationPage extends ConsumerWidget {
.read(appSettingsProvider.notifier)
.setSendOnEnter(!settings.sendOnEnter),
),
if (Platform.isAndroid) ...[
const SizedBox(height: Spacing.sm),
_CustomizationTile(
leading: _buildIconBadge(
context,
Icons.assistant,
color: theme.buttonPrimary,
),
title: l10n.androidAssistantTitle,
subtitle: assistantTriggerLabel,
onTap: () =>
_showAndroidAssistantTriggerSheet(context, ref, settings),
),
],
],
);
}
String _androidAssistantTriggerLabel(
AppLocalizations l10n,
AndroidAssistantTrigger trigger,
) {
switch (trigger) {
case AndroidAssistantTrigger.overlay:
return l10n.androidAssistantOverlayOption;
case AndroidAssistantTrigger.newChat:
return l10n.androidAssistantNewChatOption;
case AndroidAssistantTrigger.voiceCall:
return l10n.androidAssistantVoiceCallOption;
}
}
Future<void> _showAndroidAssistantTriggerSheet(
BuildContext context,
WidgetRef ref,
AppSettings settings,
) async {
final theme = context.conduitTheme;
final l10n = AppLocalizations.of(context)!;
final options = <({AndroidAssistantTrigger value, String label})>[
(
value: AndroidAssistantTrigger.overlay,
label: l10n.androidAssistantOverlayOption,
),
(
value: AndroidAssistantTrigger.newChat,
label: l10n.androidAssistantNewChatOption,
),
(
value: AndroidAssistantTrigger.voiceCall,
label: l10n.androidAssistantVoiceCallOption,
),
];
await showModalBottomSheet<void>(
context: context,
backgroundColor: theme.sidebarBackground,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(AppBorderRadius.modal),
),
),
builder: (sheetContext) {
return SafeArea(
top: false,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: Spacing.lg,
vertical: Spacing.md,
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.androidAssistantTitle,
style:
theme.headingSmall?.copyWith(
color: theme.sidebarForeground,
) ??
TextStyle(
color: theme.sidebarForeground,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: Spacing.xs),
Text(
l10n.androidAssistantDescription,
style:
theme.bodySmall?.copyWith(
color: theme.sidebarForeground.withValues(
alpha: 0.7,
),
) ??
TextStyle(
color: theme.sidebarForeground.withValues(
alpha: 0.7,
),
),
),
],
),
),
IconButton(
icon: Icon(Icons.close, color: theme.iconPrimary),
onPressed: () => Navigator.of(sheetContext).pop(),
),
],
),
),
const Divider(height: 1),
for (var i = 0; i < options.length; i++) ...[
() {
final option = options[i];
final selected =
settings.androidAssistantTrigger == option.value;
return ListTile(
leading: Icon(
selected ? Icons.check_circle : Icons.circle_outlined,
color: selected
? theme.buttonPrimary
: theme.iconSecondary,
),
title: Text(
option.label,
style: theme.bodyMedium?.copyWith(
color: theme.sidebarForeground,
fontWeight: selected
? FontWeight.w600
: FontWeight.w500,
),
),
onTap: () {
if (!selected) {
ref
.read(appSettingsProvider.notifier)
.setAndroidAssistantTrigger(option.value);
}
Navigator.of(sheetContext).pop();
},
);
}(),
if (i != options.length - 1) const Divider(height: 1),
],
const SizedBox(height: Spacing.lg),
],
),
);
},
);
}
Widget _buildSttSection(
BuildContext context,
WidgetRef ref,

View File

@@ -308,6 +308,11 @@
"chatSettings": "Chat",
"sendOnEnter": "Mit Enter senden",
"sendOnEnterDescription": "Enter sendet (Soft-Tastatur). Cmd/Ctrl+Enter ebenfalls verfügbar",
"androidAssistantTitle": "Android digital assistant",
"androidAssistantDescription": "Choose what happens when you trigger the Android digital assistant.",
"androidAssistantOverlayOption": "Show quick overlay (default)",
"androidAssistantNewChatOption": "Open Conduit with a new chat",
"androidAssistantVoiceCallOption": "Start a voice call",
"sttSettings": "Sprache zu Text",
"sttEngineLabel": "Erkennungs-Engine",
"sttEngineDevice": "Auf dem Gerät",

View File

@@ -1223,6 +1223,26 @@
"@sendOnEnterDescription": {
"description": "Explanation of how the Send on Enter toggle behaves."
},
"androidAssistantTitle": "Android digital assistant",
"@androidAssistantTitle": {
"description": "Tile title for configuring the Android digital assistant trigger."
},
"androidAssistantDescription": "Choose what happens when you trigger the Android digital assistant.",
"@androidAssistantDescription": {
"description": "Helper text describing the Android digital assistant trigger setting."
},
"androidAssistantOverlayOption": "Show quick overlay (default)",
"@androidAssistantOverlayOption": {
"description": "Option label for keeping the current assistant overlay."
},
"androidAssistantNewChatOption": "Open Conduit with a new chat",
"@androidAssistantNewChatOption": {
"description": "Option label for opening the app to a fresh chat from the assistant trigger."
},
"androidAssistantVoiceCallOption": "Start a voice call",
"@androidAssistantVoiceCallOption": {
"description": "Option label for jumping straight into voice call from assistant trigger."
},
"sttSettings": "Speech to Text",
"@sttSettings": {
"description": "Section header for speech-to-text settings."

View File

@@ -308,6 +308,11 @@
"chatSettings": "Conversación",
"sendOnEnter": "Enviar con Enter",
"sendOnEnterDescription": "Enter envía (teclado virtual). Cmd/Ctrl+Enter también disponible",
"androidAssistantTitle": "Android digital assistant",
"androidAssistantDescription": "Choose what happens when you trigger the Android digital assistant.",
"androidAssistantOverlayOption": "Show quick overlay (default)",
"androidAssistantNewChatOption": "Open Conduit with a new chat",
"androidAssistantVoiceCallOption": "Start a voice call",
"sttSettings": "Voz a texto",
"sttEngineLabel": "Motor de reconocimiento",
"sttEngineDevice": "En el dispositivo",

View File

@@ -308,6 +308,11 @@
"chatSettings": "Discussion",
"sendOnEnter": "Envoyer avec Entrée",
"sendOnEnterDescription": "Entrée envoie (clavier logiciel). Cmd/Ctrl+Entrée aussi disponible",
"androidAssistantTitle": "Android digital assistant",
"androidAssistantDescription": "Choose what happens when you trigger the Android digital assistant.",
"androidAssistantOverlayOption": "Show quick overlay (default)",
"androidAssistantNewChatOption": "Open Conduit with a new chat",
"androidAssistantVoiceCallOption": "Start a voice call",
"sttSettings": "Voix vers texte",
"sttEngineLabel": "Moteur de reconnaissance",
"sttEngineDevice": "Sur lappareil",

View File

@@ -308,6 +308,11 @@
"chatSettings": "Chat",
"sendOnEnter": "Invia con Invio",
"sendOnEnterDescription": "Invio invia (tastiera software). Cmd/Ctrl+Invio disponibile",
"androidAssistantTitle": "Android digital assistant",
"androidAssistantDescription": "Choose what happens when you trigger the Android digital assistant.",
"androidAssistantOverlayOption": "Show quick overlay (default)",
"androidAssistantNewChatOption": "Open Conduit with a new chat",
"androidAssistantVoiceCallOption": "Start a voice call",
"sttSettings": "Voce in testo",
"sttEngineLabel": "Motore di riconoscimento",
"sttEngineDevice": "Sul dispositivo",

View File

@@ -434,6 +434,11 @@
"chatSettings": "채팅",
"sendOnEnter": "Enter로 전송",
"sendOnEnterDescription": "Enter로 전송 (소프트 키보드). Cmd/Ctrl+Enter도 사용 가능",
"androidAssistantTitle": "Android digital assistant",
"androidAssistantDescription": "Choose what happens when you trigger the Android digital assistant.",
"androidAssistantOverlayOption": "Show quick overlay (default)",
"androidAssistantNewChatOption": "Open Conduit with a new chat",
"androidAssistantVoiceCallOption": "Start a voice call",
"sttSettings": "음성 텍스트 변환",
"sttEngineLabel": "인식 엔진",
"sttEngineDevice": "기기에서",

View File

@@ -308,6 +308,11 @@
"chatSettings": "Chat",
"sendOnEnter": "Verzenden met Enter",
"sendOnEnterDescription": "Enter verzendt (softtoetsenbord). Cmd/Ctrl+Enter ook beschikbaar",
"androidAssistantTitle": "Android digital assistant",
"androidAssistantDescription": "Choose what happens when you trigger the Android digital assistant.",
"androidAssistantOverlayOption": "Show quick overlay (default)",
"androidAssistantNewChatOption": "Open Conduit with a new chat",
"androidAssistantVoiceCallOption": "Start a voice call",
"sttSettings": "Spraak naar tekst",
"sttEngineLabel": "Herkenningsengine",
"sttEngineDevice": "Op het apparaat",

View File

@@ -308,6 +308,11 @@
"chatSettings": "Чат",
"sendOnEnter": "Отправка по Enter",
"sendOnEnterDescription": "Enter отправляет (программная клавиатура). Также доступно Cmd/Ctrl+Enter",
"androidAssistantTitle": "Android digital assistant",
"androidAssistantDescription": "Choose what happens when you trigger the Android digital assistant.",
"androidAssistantOverlayOption": "Show quick overlay (default)",
"androidAssistantNewChatOption": "Open Conduit with a new chat",
"androidAssistantVoiceCallOption": "Start a voice call",
"sttSettings": "Речь в текст",
"sttEngineLabel": "Движок распознавания",
"sttEngineDevice": "На устройстве",

View File

@@ -308,6 +308,11 @@
"chatSettings": "对话",
"sendOnEnter": "回车发送",
"sendOnEnterDescription": "回车发送软键盘。Cmd/Ctrl+Enter 也可用",
"androidAssistantTitle": "Android digital assistant",
"androidAssistantDescription": "Choose what happens when you trigger the Android digital assistant.",
"androidAssistantOverlayOption": "Show quick overlay (default)",
"androidAssistantNewChatOption": "Open Conduit with a new chat",
"androidAssistantVoiceCallOption": "Start a voice call",
"sttSettings": "语音转文字",
"sttEngineLabel": "识别引擎",
"sttEngineDevice": "本机",

View File

@@ -308,6 +308,11 @@
"chatSettings": "對話",
"sendOnEnter": "回車發送",
"sendOnEnterDescription": "回車發送軟鍵盤。Cmd/Ctrl+Enter 也可用",
"androidAssistantTitle": "Android digital assistant",
"androidAssistantDescription": "Choose what happens when you trigger the Android digital assistant.",
"androidAssistantOverlayOption": "Show quick overlay (default)",
"androidAssistantNewChatOption": "Open Conduit with a new chat",
"androidAssistantVoiceCallOption": "Start a voice call",
"sttSettings": "語音轉文字",
"sttEngineLabel": "識別引擎",
"sttEngineDevice": "本機",