mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-30 22:20:30 +10:00
feat: add message translation support
- Introduced translation functionality in chat screen, allowing users to translate messages before sending. - Added MessageTranslationButton to the input bar for enabling/disabling translation. - Implemented translation service to handle incoming and outgoing text translations using llama models. - Enhanced message storage to include original and translated text, language codes, and translation status. - Created UI components for displaying translated messages and managing translation options. - Added translation model management, including downloading and storing models locally. - Updated app settings to manage translation preferences and model selections.
This commit is contained in:
@@ -16,16 +16,17 @@ import '../connector/meshcore_protocol.dart';
|
||||
import '../helpers/reaction_helper.dart';
|
||||
import '../widgets/message_status_icon.dart';
|
||||
import '../helpers/chat_scroll_controller.dart';
|
||||
import '../helpers/link_handler.dart';
|
||||
import '../helpers/path_helper.dart';
|
||||
import '../helpers/utf8_length_limiter.dart';
|
||||
import '../models/channel_message.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../models/message.dart';
|
||||
import '../models/path_history.dart';
|
||||
import '../models/translation_support.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/chat_text_scale_service.dart';
|
||||
import '../services/path_history_service.dart';
|
||||
import '../services/translation_service.dart';
|
||||
import '../widgets/chat_zoom_wrapper.dart';
|
||||
import '../widgets/elements_ui.dart';
|
||||
import 'channel_message_path_screen.dart';
|
||||
@@ -35,8 +36,10 @@ import '../widgets/emoji_picker.dart';
|
||||
import '../widgets/gif_message.dart';
|
||||
import '../widgets/jump_to_bottom_button.dart';
|
||||
import '../widgets/gif_picker.dart';
|
||||
import '../widgets/message_translation_button.dart';
|
||||
import '../widgets/path_selection_dialog.dart';
|
||||
import '../widgets/radio_stats_entry.dart';
|
||||
import '../widgets/translated_message_content.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import 'telemetry_screen.dart';
|
||||
@@ -495,6 +498,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
Widget _buildInputBar(MeshCoreConnector connector) {
|
||||
final maxBytes = maxContactMessageBytes();
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final settings = context.watch<AppSettingsService>().settings;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
@@ -509,6 +513,12 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
onPressed: () => _showGifPicker(context),
|
||||
tooltip: context.l10n.chat_sendGif,
|
||||
),
|
||||
if (settings.translationEnabled)
|
||||
MessageTranslationButton(
|
||||
enabled: settings.composerTranslationEnabled,
|
||||
languageCode: settings.translationTargetLanguageCode,
|
||||
onPressed: _showTranslationOptions,
|
||||
),
|
||||
Expanded(
|
||||
child: ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: _textController,
|
||||
@@ -606,7 +616,19 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
void _sendMessage(MeshCoreConnector connector) {
|
||||
Future<void> _showTranslationOptions() async {
|
||||
final settingsService = context.read<AppSettingsService>();
|
||||
final settings = settingsService.settings;
|
||||
await showMessageTranslationSheet(
|
||||
context: context,
|
||||
enabled: settings.composerTranslationEnabled,
|
||||
selectedLanguageCode: settings.translationTargetLanguageCode,
|
||||
onEnabledChanged: settingsService.setComposerTranslationEnabled,
|
||||
onLanguageSelected: settingsService.setTranslationTargetLanguageCode,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _sendMessage(MeshCoreConnector connector) async {
|
||||
final text = _textController.text.trim();
|
||||
if (text.isEmpty) return;
|
||||
|
||||
@@ -620,17 +642,54 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
}
|
||||
_lastTextSendAt = now;
|
||||
|
||||
// Clear input synchronously to prevent double-send
|
||||
_textController.clear();
|
||||
_textFieldFocusNode.requestFocus();
|
||||
|
||||
final settings = context.read<AppSettingsService>().settings;
|
||||
final translationService = context.read<TranslationService>();
|
||||
var outgoingText = text;
|
||||
String? originalText;
|
||||
String? translatedLanguageCode;
|
||||
String? translationModelId;
|
||||
if (settings.translationEnabled) {
|
||||
final targetLanguageCode = translationService.resolvedTargetLanguageCode(
|
||||
settings.languageOverride,
|
||||
);
|
||||
if (translationService.shouldTranslateOutgoing(
|
||||
text: text,
|
||||
targetLanguageCode: targetLanguageCode,
|
||||
)) {
|
||||
final result = await translationService.translateOutgoingText(
|
||||
text: text,
|
||||
targetLanguageCode: targetLanguageCode,
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (result != null &&
|
||||
result.status == MessageTranslationStatus.completed &&
|
||||
result.translatedText.isNotEmpty) {
|
||||
outgoingText = result.translatedText;
|
||||
originalText = text;
|
||||
translatedLanguageCode = result.targetLanguageCode;
|
||||
translationModelId = result.modelId;
|
||||
}
|
||||
}
|
||||
}
|
||||
final maxBytes = maxContactMessageBytes();
|
||||
if (utf8.encode(text).length > maxBytes) {
|
||||
if (utf8.encode(outgoingText).length > maxBytes) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.chat_messageTooLong(maxBytes))),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
connector.sendMessage(_resolveContact(connector), text);
|
||||
_textController.clear();
|
||||
_textFieldFocusNode.requestFocus();
|
||||
connector.sendMessage(
|
||||
_resolveContact(connector),
|
||||
outgoingText,
|
||||
originalText: originalText,
|
||||
translatedLanguageCode: translatedLanguageCode,
|
||||
translationModelId: translationModelId,
|
||||
);
|
||||
}
|
||||
|
||||
void _showPathHistory(BuildContext context) {
|
||||
@@ -1533,6 +1592,14 @@ class _MessageBubble extends StatelessWidget {
|
||||
if (isRoomServer && !isOutgoing) {
|
||||
messageText = message.text.substring(4.clamp(0, message.text.length));
|
||||
}
|
||||
final translatedDisplayText =
|
||||
message.translatedText != null &&
|
||||
message.translatedText!.trim().isNotEmpty
|
||||
? message.translatedText!.trim()
|
||||
: messageText;
|
||||
final originalDisplayText = isOutgoing
|
||||
? message.originalText
|
||||
: (translatedDisplayText != messageText ? messageText : null);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
@@ -1662,13 +1729,17 @@ class _MessageBubble extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Flexible(
|
||||
child: LinkHandler.buildLinkifyText(
|
||||
context: context,
|
||||
text: messageText,
|
||||
child: TranslatedMessageContent(
|
||||
displayText: translatedDisplayText,
|
||||
originalText: originalDisplayText,
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: bodyFontSize * textScale,
|
||||
),
|
||||
originalStyle: TextStyle(
|
||||
color: textColor.withValues(alpha: 0.78),
|
||||
fontSize: bodyFontSize * textScale,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!enableTracing && isOutgoing) ...[
|
||||
|
||||
Reference in New Issue
Block a user