chore: update markdown dependency and refactor streaming handling
- Added `markdown` dependency version `^7.2.1` in `pubspec.yaml`. - Updated `pubspec.lock` to reflect the direct dependency change. - Refactored `streaming_helper.dart` to utilize `StreamingResponseController` for better stream management. - Enhanced `ChatMessagesNotifier` to handle message streams with improved formatting and error handling. - Updated `StreamingMarkdownWidget` to streamline markdown rendering and support new configurations.
This commit is contained in:
@@ -14,18 +14,19 @@ import '../../shared/widgets/themed_dialogs.dart';
|
||||
import '../../shared/theme/theme_extensions.dart';
|
||||
import '../utils/debug_logger.dart';
|
||||
import '../utils/openwebui_source_parser.dart';
|
||||
import 'streaming_response_controller.dart';
|
||||
|
||||
// Keep local verbosity toggle for socket logs
|
||||
const bool kSocketVerboseLogging = false;
|
||||
|
||||
class ActiveSocketStream {
|
||||
ActiveSocketStream({
|
||||
required this.streamSubscription,
|
||||
required this.controller,
|
||||
required this.socketSubscriptions,
|
||||
required this.disposeWatchdog,
|
||||
});
|
||||
|
||||
final StreamSubscription<String> streamSubscription;
|
||||
final StreamingResponseController controller;
|
||||
final List<VoidCallback> socketSubscriptions;
|
||||
final VoidCallback disposeWatchdog;
|
||||
}
|
||||
@@ -1026,8 +1027,9 @@ ActiveSocketStream attachUnifiedChunkedStreaming({
|
||||
socketSubscriptions.add(channelSub.dispose);
|
||||
}
|
||||
|
||||
final subscription = persistentController.stream.listen(
|
||||
(chunk) {
|
||||
final controller = StreamingResponseController(
|
||||
stream: persistentController.stream,
|
||||
onChunk: (chunk) {
|
||||
var effectiveChunk = chunk;
|
||||
if (webSearchEnabled && !isSearching) {
|
||||
if (chunk.contains('[SEARCHING]') ||
|
||||
@@ -1061,7 +1063,7 @@ ActiveSocketStream attachUnifiedChunkedStreaming({
|
||||
updateImagesFromCurrentContent();
|
||||
}
|
||||
},
|
||||
onDone: () async {
|
||||
onComplete: () {
|
||||
// Unregister from persistent service
|
||||
persistentService.unregisterStream(streamId);
|
||||
|
||||
@@ -1073,7 +1075,7 @@ ActiveSocketStream attachUnifiedChunkedStreaming({
|
||||
Future.microtask(refreshConversationSnapshot);
|
||||
}
|
||||
},
|
||||
onError: (error) async {
|
||||
onError: (error, stackTrace) async {
|
||||
DebugLogger.error(
|
||||
'Stream error occurred',
|
||||
scope: 'streaming/helper',
|
||||
@@ -1090,11 +1092,12 @@ ActiveSocketStream attachUnifiedChunkedStreaming({
|
||||
} catch (_) {}
|
||||
|
||||
// Check if this is a recoverable error (network issues, etc.)
|
||||
final errorText = error.toString();
|
||||
final isRecoverable =
|
||||
error is! FormatException &&
|
||||
error.toString().contains('SocketException') ||
|
||||
error.toString().contains('TimeoutException') ||
|
||||
error.toString().contains('HandshakeException');
|
||||
(error is! FormatException &&
|
||||
errorText.contains('SocketException')) ||
|
||||
errorText.contains('TimeoutException') ||
|
||||
errorText.contains('HandshakeException');
|
||||
|
||||
if (isRecoverable && socketService != null) {
|
||||
// Try to recover via socket connection if available
|
||||
@@ -1118,7 +1121,7 @@ ActiveSocketStream attachUnifiedChunkedStreaming({
|
||||
);
|
||||
|
||||
return ActiveSocketStream(
|
||||
streamSubscription: subscription,
|
||||
controller: controller,
|
||||
socketSubscriptions: socketSubscriptions,
|
||||
disposeWatchdog: () => socketWatchdog?.stop(),
|
||||
);
|
||||
|
||||
95
lib/core/services/streaming_response_controller.dart
Normal file
95
lib/core/services/streaming_response_controller.dart
Normal file
@@ -0,0 +1,95 @@
|
||||
import 'dart:async';
|
||||
|
||||
import '../utils/debug_logger.dart';
|
||||
|
||||
/// Signature for callbacks that receive streaming text updates.
|
||||
typedef StreamingChunkCallback = void Function(String chunk);
|
||||
|
||||
/// Signature for callbacks invoked when a streaming session finishes.
|
||||
typedef StreamingCompletionCallback = void Function();
|
||||
|
||||
/// Signature for callbacks invoked when a streaming session encounters an
|
||||
/// error.
|
||||
typedef StreamingErrorCallback =
|
||||
void Function(Object error, StackTrace stackTrace);
|
||||
|
||||
/// A lightweight controller that manages the lifecycle of a streamed response.
|
||||
///
|
||||
/// This wraps a [StreamSubscription], normalises error handling, and exposes
|
||||
/// a unified cancel method so UI layers can stop streaming without having to
|
||||
/// know the underlying transport (SSE, polling, etc.).
|
||||
class StreamingResponseController {
|
||||
StreamingResponseController({
|
||||
required Stream<String> stream,
|
||||
required StreamingChunkCallback onChunk,
|
||||
required StreamingCompletionCallback onComplete,
|
||||
required StreamingErrorCallback onError,
|
||||
bool cancelOnError = true,
|
||||
}) : _onChunk = onChunk,
|
||||
_onComplete = onComplete,
|
||||
_onError = onError {
|
||||
_subscription = stream.listen(
|
||||
_handleChunk,
|
||||
cancelOnError: cancelOnError,
|
||||
onDone: _handleCompleted,
|
||||
onError: _handleError,
|
||||
);
|
||||
}
|
||||
|
||||
final StreamingChunkCallback _onChunk;
|
||||
final StreamingCompletionCallback _onComplete;
|
||||
final StreamingErrorCallback _onError;
|
||||
|
||||
StreamSubscription<String>? _subscription;
|
||||
bool _isCancelled = false;
|
||||
|
||||
/// Whether the underlying stream subscription is still active.
|
||||
bool get isActive => _subscription != null && !_isCancelled;
|
||||
|
||||
void _handleChunk(String chunk) {
|
||||
if (_isCancelled) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
_onChunk(chunk);
|
||||
} catch (err, stackTrace) {
|
||||
DebugLogger.error(
|
||||
'streaming-chunk-handler-failed',
|
||||
scope: 'streaming/controller',
|
||||
error: err,
|
||||
);
|
||||
_handleError(err, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleCompleted() {
|
||||
if (_isCancelled) {
|
||||
return;
|
||||
}
|
||||
_subscription = null;
|
||||
try {
|
||||
_onComplete();
|
||||
} catch (err, stackTrace) {
|
||||
_handleError(err, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleError(Object error, StackTrace stackTrace) {
|
||||
if (_isCancelled) {
|
||||
return;
|
||||
}
|
||||
_subscription = null;
|
||||
_onError(error, stackTrace);
|
||||
}
|
||||
|
||||
/// Cancels the underlying stream subscription.
|
||||
Future<void> cancel() async {
|
||||
if (_isCancelled) {
|
||||
return;
|
||||
}
|
||||
_isCancelled = true;
|
||||
final subscription = _subscription;
|
||||
_subscription = null;
|
||||
await subscription?.cancel();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user