feat(chat): Add worker manager to streaming helper for image processing

This commit is contained in:
cogwheel0
2025-11-01 00:57:40 +05:30
parent 0b8d5b5d31
commit ada6d40e5e
6 changed files with 387 additions and 104 deletions

View File

@@ -24,6 +24,7 @@ import '../providers/chat_providers.dart' show sendMessageWithContainer;
import '../../../core/utils/debug_logger.dart';
import 'sources/openwebui_sources.dart';
import '../providers/assistant_response_builder_provider.dart';
import '../../../core/services/worker_manager.dart';
// Pre-compiled regex patterns for image processing (performance optimization)
final _base64ImagePattern = RegExp(r'data:image/[^;]+;base64,[A-Za-z0-9+/]+=*');
@@ -104,7 +105,7 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
);
// Parse reasoning and tool-calls sections
_reparseSections();
unawaited(_reparseSections());
_updateTypingIndicatorGate();
}
@@ -121,7 +122,7 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
// Re-parse sections when message content changes
if (oldWidget.message.content != widget.message.content) {
_reparseSections();
unawaited(_reparseSections());
_updateTypingIndicatorGate();
}
@@ -141,7 +142,7 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
}
}
void _reparseSections() {
Future<void> _reparseSections() async {
final raw0 = _activeVersionIndex >= 0
? (widget.message.versions[_activeVersionIndex].content as String?) ??
''
@@ -162,11 +163,13 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
final out = <MessageSegment>[];
final textBuf = StringBuffer();
final textSegments = <String>[];
if (rSegs == null || rSegs.isEmpty) {
final tSegs = ToolCallsParser.segments(raw);
if (tSegs == null || tSegs.isEmpty) {
out.add(MessageSegment.text(raw));
textBuf.write(raw);
textSegments.add(raw);
} else {
for (final s in tSegs) {
if (s.isToolCall && s.entry != null) {
@@ -174,6 +177,7 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
} else if ((s.text ?? '').isNotEmpty) {
out.add(MessageSegment.text(s.text!));
textBuf.write(s.text);
textSegments.add(s.text!);
}
}
}
@@ -187,6 +191,7 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
if (tSegs == null || tSegs.isEmpty) {
out.add(MessageSegment.text(t));
textBuf.write(t);
textSegments.add(t);
} else {
for (final s in tSegs) {
if (s.isToolCall && s.entry != null) {
@@ -194,6 +199,7 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
} else if ((s.text ?? '').isNotEmpty) {
out.add(MessageSegment.text(s.text!));
textBuf.write(s.text);
textSegments.add(s.text!);
}
}
}
@@ -202,8 +208,19 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
}
final segments = out.isEmpty ? [MessageSegment.text(raw)] : out;
final speechText = _buildTtsPlainText(segments, raw);
String speechText;
try {
final worker = ref.read(workerManagerProvider);
speechText = await worker.schedule<Map<String, dynamic>, String>(
_buildTtsPlainTextWorker,
{'segments': textSegments, 'fallback': raw},
debugLabel: 'tts_plain_text',
);
} catch (_) {
speechText = _buildTtsPlainTextFallback(textSegments, raw);
}
if (!mounted) return;
setState(() {
_segments = segments;
_ttsPlainText = speechText;
@@ -248,18 +265,14 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
}
}
String _buildTtsPlainText(List<MessageSegment> segments, String fallback) {
String _buildTtsPlainTextFallback(List<String> segments, String fallback) {
if (segments.isEmpty) {
return MarkdownToText.convert(fallback);
}
final buffer = StringBuffer();
for (final segment in segments) {
if (!segment.isText) {
continue;
}
final text = segment.text ?? '';
final sanitized = MarkdownToText.convert(text);
final sanitized = MarkdownToText.convert(segment);
if (sanitized.isEmpty) {
continue;
}
@@ -1157,7 +1170,7 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
} else if (_activeVersionIndex > 0) {
_activeVersionIndex -= 1;
}
_reparseSections();
unawaited(_reparseSections());
});
},
),
@@ -1177,7 +1190,7 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
} else {
_activeVersionIndex = -1; // move to live
}
_reparseSections();
unawaited(_reparseSections());
});
},
),
@@ -1329,6 +1342,34 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
}
}
String _buildTtsPlainTextWorker(Map<String, dynamic> payload) {
final rawSegments = payload['segments'];
final fallback = payload['fallback'] as String? ?? '';
final segments = rawSegments is List ? rawSegments.cast<dynamic>() : const [];
if (segments.isEmpty) {
return MarkdownToText.convert(fallback);
}
final buffer = StringBuffer();
for (final segment in segments) {
if (segment is! String || segment.isEmpty) continue;
final sanitized = MarkdownToText.convert(segment);
if (sanitized.isEmpty) continue;
if (buffer.isNotEmpty) {
buffer.writeln();
buffer.writeln();
}
buffer.write(sanitized);
}
final result = buffer.toString().trim();
if (result.isEmpty) {
return MarkdownToText.convert(fallback);
}
return result;
}
class StatusHistoryTimeline extends StatefulWidget {
const StatusHistoryTimeline({
super.key,

View File

@@ -9,6 +9,7 @@ import 'package:share_plus/share_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
import 'dart:convert';
import '../../../core/services/worker_manager.dart';
class EnhancedAttachment extends ConsumerStatefulWidget {
final String attachmentId;
@@ -102,12 +103,14 @@ class _EnhancedAttachmentState extends ConsumerState<EnhancedAttachment> {
final dir = await getTemporaryDirectory();
final filePath = '${dir.path}/$filename';
final worker = ref.read(workerManagerProvider);
try {
if (content.length > 128 &&
RegExp(
r'^[A-Za-z0-9+/=\r\n]+$',
).hasMatch(content.replaceAll('\n', ''))) {
final bytes = base64Decode(content.replaceAll('\n', ''));
if (_looksLikeBase64(content)) {
final bytes = await worker.schedule<String, Uint8List>(
_decodeAttachmentBase64,
content,
debugLabel: 'attachment_decode_bytes',
);
await File(filePath).writeAsBytes(bytes, flush: true);
} else {
await File(filePath).writeAsString(content, flush: true);
@@ -291,3 +294,14 @@ class _EnhancedAttachmentState extends ConsumerState<EnhancedAttachment> {
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
}
}
bool _looksLikeBase64(String content) {
if (content.length <= 128) return false;
final sanitized = content.replaceAll('\n', '');
return RegExp(r'^[A-Za-z0-9+/=]+$').hasMatch(sanitized);
}
Uint8List _decodeAttachmentBase64(String raw) {
final sanitized = raw.replaceAll('\n', '');
return base64Decode(sanitized);
}

View File

@@ -1,6 +1,6 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:cached_network_image/cached_network_image.dart';
@@ -15,6 +15,7 @@ import '../../auth/providers/unified_auth_providers.dart';
import '../../../core/utils/debug_logger.dart';
import '../../../core/network/self_signed_image_cache_manager.dart';
import '../../../core/network/image_header_utils.dart';
import '../../../core/services/worker_manager.dart';
// Simple global cache to prevent reloading
final _globalImageCache = <String, String>{};
@@ -23,13 +24,6 @@ final _globalErrorStates = <String, String>{};
final _globalImageBytesCache = <String, Uint8List>{};
final _base64WhitespacePattern = RegExp(r'\s');
Future<Uint8List> _decodeImageDataAsync(String data) async {
if (kIsWeb) {
return _decodeImageData(data);
}
return compute(_decodeImageData, data);
}
Uint8List _decodeImageData(String data) {
var payload = data;
if (payload.startsWith('data:')) {
@@ -233,7 +227,12 @@ class _EnhancedImageAttachmentState
if (_isDecoding) return;
_isDecoding = true;
try {
final bytes = await _decodeImageDataAsync(data);
final worker = ref.read(workerManagerProvider);
final bytes = await worker.schedule(
_decodeImageData,
data,
debugLabel: 'decode_image',
);
_globalImageBytesCache[widget.attachmentId] = bytes;
if (!mounted) return;
setState(() {