refactor: more logs

This commit is contained in:
cogwheel0
2025-09-25 23:22:48 +05:30
parent 9210b2155a
commit 3124bccfeb
20 changed files with 937 additions and 846 deletions

View File

@@ -1,4 +1,4 @@
import 'package:flutter/foundation.dart' hide debugPrint; import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
// Types are used through app_providers.dart // Types are used through app_providers.dart
import '../providers/app_providers.dart'; import '../providers/app_providers.dart';
@@ -7,11 +7,6 @@ import 'token_validator.dart';
import 'auth_cache_manager.dart'; import 'auth_cache_manager.dart';
import '../utils/debug_logger.dart'; import '../utils/debug_logger.dart';
void debugPrint(String? message, {int? wrapWidth}) {
if (message == null) return;
DebugLogger.fromLegacy(message, scope: 'auth/state');
}
/// Comprehensive auth state representation /// Comprehensive auth state representation
@immutable @immutable
class AuthState { class AuthState {
@@ -153,7 +148,7 @@ class AuthStateManager extends Notifier<AuthState> {
); );
} }
} catch (e) { } catch (e) {
debugPrint('ERROR: Auth initialization failed: $e'); DebugLogger.error('auth-init-failed', scope: 'auth/state', error: e);
state = state.copyWith( state = state.copyWith(
status: AuthStatus.error, status: AuthStatus.error,
error: 'Failed to initialize auth: $e', error: 'Failed to initialize auth: $e',
@@ -244,7 +239,7 @@ class AuthStateManager extends Notifier<AuthState> {
throw Exception('Invalid API key or insufficient permissions'); throw Exception('Invalid API key or insufficient permissions');
} }
} catch (e) { } catch (e) {
debugPrint('ERROR: API key login failed: $e'); DebugLogger.error('api-key-login-failed', scope: 'auth/state', error: e);
state = state.copyWith( state = state.copyWith(
status: AuthStatus.error, status: AuthStatus.error,
error: e.toString(), error: e.toString(),
@@ -325,7 +320,7 @@ class AuthStateManager extends Notifier<AuthState> {
DebugLogger.auth('Login successful'); DebugLogger.auth('Login successful');
return true; return true;
} catch (e) { } catch (e) {
debugPrint('ERROR: Login failed: $e'); DebugLogger.error('login-failed', scope: 'auth/state', error: e);
state = state.copyWith( state = state.copyWith(
status: AuthStatus.error, status: AuthStatus.error,
error: e.toString(), error: e.toString(),
@@ -433,7 +428,7 @@ class AuthStateManager extends Notifier<AuthState> {
return await login(username, password, rememberCredentials: false); return await login(username, password, rememberCredentials: false);
} }
} catch (e) { } catch (e) {
debugPrint('ERROR: Silent login failed: $e'); DebugLogger.error('silent-login-failed', scope: 'auth/state', error: e);
// Clear invalid credentials on auth errors // Clear invalid credentials on auth errors
if (e.toString().contains('401') || if (e.toString().contains('401') ||
@@ -496,7 +491,11 @@ class AuthStateManager extends Notifier<AuthState> {
try { try {
await api.logout(); await api.logout();
} catch (e) { } catch (e) {
debugPrint('Warning: Server logout failed: $e'); DebugLogger.warning(
'server-logout-failed',
scope: 'auth/state',
data: {'error': e.toString()},
);
} }
} }
@@ -516,7 +515,7 @@ class AuthStateManager extends Notifier<AuthState> {
DebugLogger.auth('Logout complete'); DebugLogger.auth('Logout complete');
} catch (e) { } catch (e) {
debugPrint('ERROR: Logout failed: $e'); DebugLogger.error('logout-failed', scope: 'auth/state', error: e);
// Even if logout fails, clear local state // Even if logout fails, clear local state
state = state.copyWith( state = state.copyWith(
status: AuthStatus.unauthenticated, status: AuthStatus.unauthenticated,
@@ -551,7 +550,11 @@ class AuthStateManager extends Notifier<AuthState> {
// Fall back to server data loading // Fall back to server data loading
await _loadServerUserData(); await _loadServerUserData();
} catch (e) { } catch (e) {
debugPrint('Warning: Failed to load user data: $e'); DebugLogger.warning(
'user-data-load-failed',
scope: 'auth/state',
data: {'error': e.toString()},
);
// Don't update state on user data load failure // Don't update state on user data load failure
} }
} }
@@ -563,9 +566,7 @@ class AuthStateManager extends Notifier<AuthState> {
if (api != null && state.isAuthenticated) { if (api != null && state.isAuthenticated) {
// Check if we already have user data from token validation // Check if we already have user data from token validation
if (state.user != null) { if (state.user != null) {
debugPrint( DebugLogger.auth('user-data-present-from-token', scope: 'auth/state');
'DEBUG: User data already available from token validation',
);
return; return;
} }
@@ -574,7 +575,11 @@ class AuthStateManager extends Notifier<AuthState> {
DebugLogger.auth('Loaded complete user data from server'); DebugLogger.auth('Loaded complete user data from server');
} }
} catch (e) { } catch (e) {
debugPrint('Warning: Failed to load server user data: $e'); DebugLogger.warning(
'server-user-data-load-failed',
scope: 'auth/state',
data: {'error': e.toString()},
);
// Don't update state on server data load failure - keep JWT data if available // Don't update state on server data load failure - keep JWT data if available
} }
} }
@@ -605,21 +610,25 @@ class AuthStateManager extends Notifier<AuthState> {
// Fast format validation first // Fast format validation first
final formatResult = TokenValidator.validateTokenFormat(token); final formatResult = TokenValidator.validateTokenFormat(token);
if (!formatResult.isValid) { if (!formatResult.isValid) {
debugPrint('DEBUG: Token format invalid: ${formatResult.message}'); DebugLogger.warning(
'token-format-invalid',
scope: 'auth/state',
data: {'message': formatResult.message},
);
TokenValidationCache.cacheResult(token, formatResult); TokenValidationCache.cacheResult(token, formatResult);
return false; return false;
} }
// If format is valid but token is expiring soon, try server validation // If format is valid but token is expiring soon, try server validation
if (formatResult.isExpiringSoon) { if (formatResult.isExpiringSoon) {
debugPrint('DEBUG: Token expiring soon, validating with server'); DebugLogger.auth('token-expiring-soon', scope: 'auth/state');
} }
// Server validation (async with timeout) // Server validation (async with timeout)
try { try {
final api = ref.read(apiServiceProvider); final api = ref.read(apiServiceProvider);
if (api == null) { if (api == null) {
debugPrint('DEBUG: No API service available for token validation'); DebugLogger.warning('token-validation-no-api', scope: 'auth/state');
return formatResult.isValid; // Fall back to format validation return formatResult.isValid; // Fall back to format validation
} }
@@ -650,7 +659,11 @@ class AuthStateManager extends Notifier<AuthState> {
); );
return serverResult.isValid; return serverResult.isValid;
} catch (e) { } catch (e) {
debugPrint('DEBUG: Token server validation failed: $e'); DebugLogger.warning(
'token-validation-failed',
scope: 'auth/state',
data: {'error': e.toString()},
);
// On network error, fall back to format validation if it was valid // On network error, fall back to format validation if it was valid
return formatResult.isValid; return formatResult.isValid;
} }

View File

@@ -2,11 +2,6 @@ import 'dart:convert';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import '../utils/debug_logger.dart'; import '../utils/debug_logger.dart';
void debugPrint(String? message, {int? wrapWidth}) {
if (message == null) return;
DebugLogger.fromLegacy(message, scope: 'auth/token-validator');
}
/// JWT token validation utilities /// JWT token validation utilities
class TokenValidator { class TokenValidator {
static const Duration _validationTimeout = Duration(seconds: 5); static const Duration _validationTimeout = Duration(seconds: 5);
@@ -68,8 +63,10 @@ class TokenValidator {
); );
} catch (e) { } catch (e) {
// If we can't decode JWT, treat as opaque token // If we can't decode JWT, treat as opaque token
debugPrint( DebugLogger.warning(
'DEBUG: Could not decode JWT payload, treating as opaque token: $e', 'jwt-decode-failed',
scope: 'auth/token-validator',
data: {'error': e.toString()},
); );
return TokenValidationResult.valid('Opaque token format valid'); return TokenValidationResult.valid('Opaque token format valid');
} }
@@ -153,7 +150,11 @@ class TokenValidator {
'iat': payload['iat'], // Issued at 'iat': payload['iat'], // Issued at
}; };
} catch (e) { } catch (e) {
debugPrint('DEBUG: Could not extract user info from token: $e'); DebugLogger.warning(
'token-user-info-failed',
scope: 'auth/token-validator',
data: {'error': e.toString()},
);
return null; return null;
} }
} }

View File

@@ -1,14 +1,9 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart' hide debugPrint; import 'package:flutter/foundation.dart';
import 'api_error.dart'; import 'api_error.dart';
import 'error_parser.dart'; import 'error_parser.dart';
import '../utils/debug_logger.dart'; import '../utils/debug_logger.dart';
void debugPrint(String? message, {int? wrapWidth}) {
if (message == null) return;
DebugLogger.fromLegacy(message, scope: 'api/error-handler');
}
/// Comprehensive API error handler with structured error parsing /// Comprehensive API error handler with structured error parsing
/// Handles all types of API errors and converts them to standardized format /// Handles all types of API errors and converts them to standardized format
class ApiErrorHandler { class ApiErrorHandler {
@@ -39,7 +34,10 @@ class ApiErrorHandler {
} }
} catch (e) { } catch (e) {
// Fallback error if transformation itself fails // Fallback error if transformation itself fails
debugPrint('ApiErrorHandler: Error transforming exception: $e'); DebugLogger.log(
'ApiErrorHandler: Error transforming exception: $e',
scope: 'api/error-handler',
);
return ApiError.unknown( return ApiError.unknown(
message: 'A system error occurred', message: 'A system error occurred',
originalError: error, originalError: error,
@@ -318,21 +316,33 @@ class ApiErrorHandler {
String httpMethod, String httpMethod,
) { ) {
if (kDebugMode) { if (kDebugMode) {
debugPrint('🔴 API Error Details:'); DebugLogger.log('🔴 API Error Details:', scope: 'api/error-handler');
debugPrint(' Method: ${httpMethod.toUpperCase()}'); DebugLogger.log(
debugPrint(' Endpoint: $requestPath'); ' Method: ${httpMethod.toUpperCase()}',
debugPrint(' Type: ${dioError.type}'); scope: 'api/error-handler',
debugPrint(' Status: ${dioError.response?.statusCode}'); );
DebugLogger.log(' Endpoint: $requestPath', scope: 'api/error-handler');
DebugLogger.log(' Type: ${dioError.type}', scope: 'api/error-handler');
DebugLogger.log(
' Status: ${dioError.response?.statusCode}',
scope: 'api/error-handler',
);
if (dioError.response?.data != null) { if (dioError.response?.data != null) {
DebugLogger.error('Response data available (truncated for security)'); DebugLogger.error('Response data available (truncated for security)');
} }
if (dioError.requestOptions.data != null) { if (dioError.requestOptions.data != null) {
debugPrint(' Request Data: ${dioError.requestOptions.data}'); DebugLogger.log(
' Request Data: ${dioError.requestOptions.data}',
scope: 'api/error-handler',
);
} }
debugPrint(' Error: ${dioError.message}'); DebugLogger.log(
' Error: ${dioError.message}',
scope: 'api/error-handler',
);
} }
// In production, you would send this to your error tracking service // In production, you would send this to your error tracking service

View File

@@ -1,14 +1,9 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart' hide debugPrint; import 'package:flutter/foundation.dart';
import 'api_error_handler.dart'; import 'api_error_handler.dart';
import 'api_error.dart'; import 'api_error.dart';
import '../utils/debug_logger.dart'; import '../utils/debug_logger.dart';
void debugPrint(String? message, {int? wrapWidth}) {
if (message == null) return;
DebugLogger.fromLegacy(message, scope: 'api/error-interceptor');
}
/// Dio interceptor for automatic error handling and transformation /// Dio interceptor for automatic error handling and transformation
/// Converts all HTTP errors into standardized ApiError format /// Converts all HTTP errors into standardized ApiError format
class ApiErrorInterceptor extends Interceptor { class ApiErrorInterceptor extends Interceptor {
@@ -51,7 +46,11 @@ class ApiErrorInterceptor extends Interceptor {
} }
} catch (e) { } catch (e) {
// Fallback if error transformation fails // Fallback if error transformation fails
debugPrint('ApiErrorInterceptor: Failed to transform error: $e'); DebugLogger.error(
'transform-failed',
scope: 'api/error-interceptor',
error: e,
);
handler.next(err); handler.next(err);
} }
} }
@@ -71,7 +70,16 @@ class ApiErrorInterceptor extends Interceptor {
); );
if (logErrors) { if (logErrors) {
debugPrint('🟡 API Error in successful response: $apiError'); DebugLogger.warning(
'successful-response-error',
scope: 'api/error-interceptor',
data: {
'endpoint': apiError.endpoint,
'method': apiError.method,
'status': apiError.statusCode,
'message': apiError.message,
},
);
} }
// Store the error for later handling // Store the error for later handling
@@ -122,74 +130,59 @@ class ApiErrorInterceptor extends Interceptor {
void _logApiError(ApiError apiError, DioException originalError) { void _logApiError(ApiError apiError, DioException originalError) {
if (!kDebugMode) return; if (!kDebugMode) return;
final typeIcon = _getErrorTypeIcon(apiError.type); final payload = <String, Object?>{
debugPrint('$typeIcon API Error [${apiError.type.name.toUpperCase()}]'); 'type': apiError.type.name,
debugPrint(' Method: ${apiError.method?.toUpperCase() ?? 'UNKNOWN'}'); 'endpoint': apiError.endpoint,
debugPrint(' Endpoint: ${apiError.endpoint ?? 'unknown'}'); 'method': apiError.method,
debugPrint(' Status: ${apiError.statusCode ?? 'N/A'}'); 'status': apiError.statusCode,
debugPrint(' Message: ${apiError.message}'); 'message': apiError.message,
if (apiError.technical != null) 'technical': apiError.technical,
if (apiError.retryAfter != null)
'retryAfterSeconds': apiError.retryAfter!.inSeconds,
'originalType': originalError.type.name,
};
if (apiError.hasFieldErrors) { if (apiError.hasFieldErrors) {
debugPrint(' Field Errors:'); payload['fieldErrors'] = {
for (final entry in apiError.fieldErrors.entries) { for (final entry in apiError.fieldErrors.entries)
final field = entry.key; entry.key: entry.value,
final errors = entry.value; };
debugPrint(' $field: ${errors.join(', ')}');
}
} }
if (apiError.technical != null) {
debugPrint(' Technical: ${apiError.technical}');
}
if (apiError.retryAfter != null) {
debugPrint(' Retry After: ${apiError.retryAfter!.inSeconds}s');
}
// Log original error type for debugging
debugPrint(' Original Type: ${originalError.type}');
// Log request details if available
final requestData = originalError.requestOptions.data; final requestData = originalError.requestOptions.data;
if (requestData != null && requestData.toString().length < 500) { if (requestData != null && requestData.toString().length < 500) {
debugPrint(' Request: $requestData'); payload['request'] = requestData;
} }
// Log response data if available and not too large
final responseData = originalError.response?.data; final responseData = originalError.response?.data;
if (responseData != null && responseData.toString().length < 1000) { if (responseData != null && responseData.toString().length < 1000) {
DebugLogger.error('Response data available (truncated for security)'); payload['response'] = responseData;
} }
}
/// Get emoji icon for error type final headers = originalError.requestOptions.headers;
String _getErrorTypeIcon(ApiErrorType type) { if (headers.isNotEmpty) {
switch (type) { payload['requestHeaders'] = {
case ApiErrorType.network: for (final entry in headers.entries) entry.key: entry.value.toString(),
return '🌐'; };
case ApiErrorType.timeout:
return '⏱️';
case ApiErrorType.authentication:
return '🔐';
case ApiErrorType.authorization:
return '🚫';
case ApiErrorType.validation:
return '✏️';
case ApiErrorType.badRequest:
return '';
case ApiErrorType.notFound:
return '🔍';
case ApiErrorType.server:
return '🔥';
case ApiErrorType.rateLimit:
return '🐌';
case ApiErrorType.cancelled:
return '🛑';
case ApiErrorType.security:
return '🔒';
case ApiErrorType.unknown:
return '';
} }
final responseHeaders = originalError.response?.headers;
if (responseHeaders != null && responseHeaders.map.isNotEmpty) {
payload['responseHeaders'] = responseHeaders.map;
}
final requestDuration =
originalError.response?.requestOptions.extra['requestDuration'];
if (requestDuration is Duration) {
payload['requestDurationMs'] = requestDuration.inMilliseconds;
}
DebugLogger.error(
'api-error',
scope: 'api/error-interceptor',
data: payload,
error: apiError,
);
} }
/// Extract ApiError from DioException if available /// Extract ApiError from DioException if available

View File

@@ -1,5 +1,5 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart' hide debugPrint; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'api_error.dart'; import 'api_error.dart';
import 'api_error_handler.dart'; import 'api_error_handler.dart';
@@ -9,11 +9,6 @@ import '../../shared/theme/theme_extensions.dart';
import 'package:conduit/l10n/app_localizations.dart'; import 'package:conduit/l10n/app_localizations.dart';
import '../utils/debug_logger.dart'; import '../utils/debug_logger.dart';
void debugPrint(String? message, {int? wrapWidth}) {
if (message == null) return;
DebugLogger.fromLegacy(message, scope: 'api/error-service');
}
/// Enhanced error service with comprehensive error handling capabilities /// Enhanced error service with comprehensive error handling capabilities
/// Provides unified error management across the application /// Provides unified error management across the application
class EnhancedErrorService { class EnhancedErrorService {
@@ -306,16 +301,31 @@ class EnhancedErrorService {
}) { }) {
if (kDebugMode) { if (kDebugMode) {
final timestamp = DateTime.now().toIso8601String(); final timestamp = DateTime.now().toIso8601String();
debugPrint('🔴 ERROR [$timestamp] ${context ?? 'Unknown Context'}'); DebugLogger.log(
debugPrint(' Message: ${getUserMessage(error)}'); '🔴 ERROR [$timestamp] ${context ?? 'Unknown Context'}',
debugPrint(' Technical: ${getTechnicalDetails(error)}'); scope: 'api/error-service',
);
DebugLogger.log(
' Message: ${getUserMessage(error)}',
scope: 'api/error-service',
);
DebugLogger.log(
' Technical: ${getTechnicalDetails(error)}',
scope: 'api/error-service',
);
if (additionalData != null && additionalData.isNotEmpty) { if (additionalData != null && additionalData.isNotEmpty) {
debugPrint(' Additional Data: $additionalData'); DebugLogger.log(
' Additional Data: $additionalData',
scope: 'api/error-service',
);
} }
if (stackTrace != null) { if (stackTrace != null) {
debugPrint(' Stack Trace: $stackTrace'); DebugLogger.log(
' Stack Trace: $stackTrace',
scope: 'api/error-service',
);
} }
} }

View File

@@ -1,11 +1,6 @@
import 'api_error.dart'; import 'api_error.dart';
import '../utils/debug_logger.dart'; import '../utils/debug_logger.dart';
void debugPrint(String? message, {int? wrapWidth}) {
if (message == null) return;
DebugLogger.fromLegacy(message, scope: 'api/error-parser');
}
/// Comprehensive error response parser /// Comprehensive error response parser
/// Handles various API error response formats and extracts structured information /// Handles various API error response formats and extracts structured information
class ErrorParser { class ErrorParser {
@@ -29,7 +24,10 @@ class ErrorParser {
); );
} }
} catch (e) { } catch (e) {
debugPrint('ErrorParser: Error parsing response: $e'); DebugLogger.log(
'ErrorParser: Error parsing response: $e',
scope: 'api/error-parser',
);
return ParsedErrorResponse( return ParsedErrorResponse(
message: 'Failed to parse error response', message: 'Failed to parse error response',
metadata: { metadata: {

File diff suppressed because it is too large Load Diff

View File

@@ -5,11 +5,6 @@ import 'package:dio/dio.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../utils/debug_logger.dart'; import '../utils/debug_logger.dart';
void debugPrint(String? message, {int? wrapWidth}) {
if (message == null) return;
DebugLogger.fromLegacy(message, scope: 'attachments/queue');
}
/// Status of a queued attachment upload /// Status of a queued attachment upload
enum QueuedAttachmentStatus { pending, uploading, completed, failed, cancelled } enum QueuedAttachmentStatus { pending, uploading, completed, failed, cancelled }
@@ -144,8 +139,9 @@ class AttachmentUploadQueue {
_prefs ??= await SharedPreferences.getInstance(); _prefs ??= await SharedPreferences.getInstance();
await _load(); await _load();
_startPeriodicProcessing(); _startPeriodicProcessing();
debugPrint( DebugLogger.log(
'DEBUG: AttachmentUploadQueue initialized with ${_queue.length} items', 'AttachmentUploadQueue initialized with ${_queue.length} items',
scope: 'attachments/queue',
); );
} }
@@ -223,8 +219,9 @@ class AttachmentUploadQueue {
await _save(); await _save();
_notify(); _notify();
debugPrint( DebugLogger.log(
'DEBUG: Attachment ${item.id} uploaded successfully (fileId=$fileId)', 'Attachment ${item.id} uploaded successfully (fileId=$fileId)',
scope: 'attachments/queue',
); );
} catch (e) { } catch (e) {
final retries = item.retryCount + 1; final retries = item.retryCount + 1;
@@ -239,8 +236,9 @@ class AttachmentUploadQueue {
); );
await _save(); await _save();
_notify(); _notify();
debugPrint( DebugLogger.log(
'WARNING: Attachment ${item.id} failed after $_maxRetries attempts', 'WARNING: Attachment ${item.id} failed after $_maxRetries attempts',
scope: 'attachments/queue',
); );
return; return;
} }
@@ -257,8 +255,9 @@ class AttachmentUploadQueue {
); );
await _save(); await _save();
_notify(); _notify();
debugPrint( DebugLogger.log(
'DEBUG: Scheduled retry for attachment ${item.id} in ${delay.inSeconds}s', 'Scheduled retry for attachment ${item.id} in ${delay.inSeconds}s',
scope: 'attachments/queue',
); );
} }
} }

View File

@@ -6,11 +6,6 @@ import '../models/server_config.dart';
import '../models/conversation.dart'; import '../models/conversation.dart';
import '../utils/debug_logger.dart'; import '../utils/debug_logger.dart';
void debugPrint(String? message, {int? wrapWidth}) {
if (message == null) return;
DebugLogger.fromLegacy(message, scope: 'storage/optimized');
}
/// Optimized storage service with single secure storage implementation /// Optimized storage service with single secure storage implementation
/// Eliminates dual storage overhead and improves performance /// Eliminates dual storage overhead and improves performance
class OptimizedStorageService { class OptimizedStorageService {
@@ -46,9 +41,15 @@ class OptimizedStorageService {
await _secureCredentialStorage.saveAuthToken(token); await _secureCredentialStorage.saveAuthToken(token);
_cache[_authTokenKey] = token; _cache[_authTokenKey] = token;
_cacheTimestamps[_authTokenKey] = DateTime.now(); _cacheTimestamps[_authTokenKey] = DateTime.now();
debugPrint('DEBUG: Auth token saved and cached'); DebugLogger.log(
'Auth token saved and cached',
scope: 'storage/optimized',
);
} catch (e) { } catch (e) {
debugPrint('ERROR: Failed to save auth token: $e'); DebugLogger.log(
'Failed to save auth token: $e',
scope: 'storage/optimized',
);
rethrow; rethrow;
} }
} }
@@ -58,7 +59,7 @@ class OptimizedStorageService {
if (_isCacheValid(_authTokenKey)) { if (_isCacheValid(_authTokenKey)) {
final cachedToken = _cache[_authTokenKey] as String?; final cachedToken = _cache[_authTokenKey] as String?;
if (cachedToken != null) { if (cachedToken != null) {
debugPrint('DEBUG: Using cached auth token'); DebugLogger.log('Using cached auth token', scope: 'storage/optimized');
return cachedToken; return cachedToken;
} }
} }
@@ -71,7 +72,10 @@ class OptimizedStorageService {
} }
return token; return token;
} catch (e) { } catch (e) {
debugPrint('ERROR: Failed to retrieve auth token: $e'); DebugLogger.log(
'Failed to retrieve auth token: $e',
scope: 'storage/optimized',
);
return null; return null;
} }
} }
@@ -81,9 +85,15 @@ class OptimizedStorageService {
await _secureCredentialStorage.deleteAuthToken(); await _secureCredentialStorage.deleteAuthToken();
_cache.remove(_authTokenKey); _cache.remove(_authTokenKey);
_cacheTimestamps.remove(_authTokenKey); _cacheTimestamps.remove(_authTokenKey);
debugPrint('DEBUG: Auth token deleted and cache cleared'); DebugLogger.log(
'Auth token deleted and cache cleared',
scope: 'storage/optimized',
);
} catch (e) { } catch (e) {
debugPrint('ERROR: Failed to delete auth token: $e'); DebugLogger.log(
'Failed to delete auth token: $e',
scope: 'storage/optimized',
);
} }
} }
@@ -104,9 +114,15 @@ class OptimizedStorageService {
_cache['has_credentials'] = true; _cache['has_credentials'] = true;
_cacheTimestamps['has_credentials'] = DateTime.now(); _cacheTimestamps['has_credentials'] = DateTime.now();
debugPrint('DEBUG: Credentials saved via optimized storage'); DebugLogger.log(
'Credentials saved via optimized storage',
scope: 'storage/optimized',
);
} catch (e) { } catch (e) {
debugPrint('ERROR: Failed to save credentials: $e'); DebugLogger.log(
'Failed to save credentials: $e',
scope: 'storage/optimized',
);
rethrow; rethrow;
} }
} }
@@ -122,7 +138,10 @@ class OptimizedStorageService {
return credentials; return credentials;
} catch (e) { } catch (e) {
debugPrint('ERROR: Failed to retrieve credentials: $e'); DebugLogger.log(
'Failed to retrieve credentials: $e',
scope: 'storage/optimized',
);
return null; return null;
} }
} }
@@ -132,9 +151,15 @@ class OptimizedStorageService {
await _secureCredentialStorage.deleteSavedCredentials(); await _secureCredentialStorage.deleteSavedCredentials();
_cache.remove('has_credentials'); _cache.remove('has_credentials');
_cacheTimestamps.remove('has_credentials'); _cacheTimestamps.remove('has_credentials');
debugPrint('DEBUG: Credentials deleted via optimized storage'); DebugLogger.log(
'Credentials deleted via optimized storage',
scope: 'storage/optimized',
);
} catch (e) { } catch (e) {
debugPrint('ERROR: Failed to delete credentials: $e'); DebugLogger.log(
'Failed to delete credentials: $e',
scope: 'storage/optimized',
);
} }
} }
@@ -167,9 +192,15 @@ class OptimizedStorageService {
_cache['server_config_count'] = configs.length; _cache['server_config_count'] = configs.length;
_cacheTimestamps['server_config_count'] = DateTime.now(); _cacheTimestamps['server_config_count'] = DateTime.now();
debugPrint('DEBUG: Server configs saved (${configs.length} configs)'); DebugLogger.log(
'Server configs saved (${configs.length} configs)',
scope: 'storage/optimized',
);
} catch (e) { } catch (e) {
debugPrint('ERROR: Failed to save server configs: $e'); DebugLogger.log(
'Failed to save server configs: $e',
scope: 'storage/optimized',
);
rethrow; rethrow;
} }
} }
@@ -194,7 +225,10 @@ class OptimizedStorageService {
return configs; return configs;
} catch (e) { } catch (e) {
debugPrint('ERROR: Failed to retrieve server configs: $e'); DebugLogger.log(
'Failed to retrieve server configs: $e',
scope: 'storage/optimized',
);
return []; return [];
} }
} }
@@ -275,7 +309,10 @@ class OptimizedStorageService {
final decoded = jsonDecode(jsonString) as List<dynamic>; final decoded = jsonDecode(jsonString) as List<dynamic>;
return decoded.map((item) => Conversation.fromJson(item)).toList(); return decoded.map((item) => Conversation.fromJson(item)).toList();
} catch (e) { } catch (e) {
debugPrint('ERROR: Failed to retrieve local conversations: $e'); DebugLogger.log(
'Failed to retrieve local conversations: $e',
scope: 'storage/optimized',
);
return []; return [];
} }
} }
@@ -298,11 +335,15 @@ class OptimizedStorageService {
final jsonString = jsonEncode(lightweightConversations); final jsonString = jsonEncode(lightweightConversations);
await _prefs.setString(_localConversationsKey, jsonString); await _prefs.setString(_localConversationsKey, jsonString);
debugPrint( DebugLogger.log(
'DEBUG: Saved ${conversations.length} local conversations (lightweight)', 'Saved ${conversations.length} local conversations (lightweight)',
scope: 'storage/optimized',
); );
} catch (e) { } catch (e) {
debugPrint('ERROR: Failed to save local conversations: $e'); DebugLogger.log(
'Failed to save local conversations: $e',
scope: 'storage/optimized',
);
} }
} }
@@ -331,9 +372,15 @@ class OptimizedStorageService {
key.contains('server'), key.contains('server'),
); );
debugPrint('DEBUG: Auth data cleared in batch operation'); DebugLogger.log(
'Auth data cleared in batch operation',
scope: 'storage/optimized',
);
} catch (e) { } catch (e) {
debugPrint('ERROR: Failed to clear auth data: $e'); DebugLogger.log(
'Failed to clear auth data: $e',
scope: 'storage/optimized',
);
} }
} }
@@ -344,9 +391,12 @@ class OptimizedStorageService {
_cache.clear(); _cache.clear();
_cacheTimestamps.clear(); _cacheTimestamps.clear();
debugPrint('DEBUG: All storage cleared'); DebugLogger.log('All storage cleared', scope: 'storage/optimized');
} catch (e) { } catch (e) {
debugPrint('ERROR: Failed to clear all storage: $e'); DebugLogger.log(
'Failed to clear all storage: $e',
scope: 'storage/optimized',
);
} }
} }
@@ -366,21 +416,30 @@ class OptimizedStorageService {
void clearCache() { void clearCache() {
_cache.clear(); _cache.clear();
_cacheTimestamps.clear(); _cacheTimestamps.clear();
debugPrint('DEBUG: Storage cache cleared'); DebugLogger.log('Storage cache cleared', scope: 'storage/optimized');
} }
/// Migration from old storage service (one-time operation) /// Migration from old storage service (one-time operation)
Future<void> migrateFromLegacyStorage() async { Future<void> migrateFromLegacyStorage() async {
try { try {
debugPrint('DEBUG: Starting migration from legacy storage'); DebugLogger.log(
'Starting migration from legacy storage',
scope: 'storage/optimized',
);
// This would be called once during app upgrade // This would be called once during app upgrade
// Implementation would depend on the specific migration needs // Implementation would depend on the specific migration needs
// For now, the SecureCredentialStorage already handles legacy migration // For now, the SecureCredentialStorage already handles legacy migration
debugPrint('DEBUG: Legacy storage migration completed'); DebugLogger.log(
'Legacy storage migration completed',
scope: 'storage/optimized',
);
} catch (e) { } catch (e) {
debugPrint('ERROR: Legacy storage migration failed: $e'); DebugLogger.log(
'Legacy storage migration failed: $e',
scope: 'storage/optimized',
);
} }
} }

View File

@@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart' hide debugPrint; import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:share_handler/share_handler.dart' as sh; import 'package:share_handler/share_handler.dart' as sh;
@@ -85,7 +85,10 @@ final shareReceiverInitializerProvider = Provider<void>((ref) {
maybeProcessPending(); maybeProcessPending();
} }
} catch (e) { } catch (e) {
debugPrint('ShareReceiver: failed to get initial shared media: $e'); DebugLogger.log(
'ShareReceiver: failed to get initial shared media: $e',
scope: 'share',
);
} }
}); });
@@ -98,7 +101,10 @@ final shareReceiverInitializerProvider = Provider<void>((ref) {
maybeProcessPending(); maybeProcessPending();
} }
} catch (e) { } catch (e) {
debugPrint('ShareReceiver: failed to parse shared media: $e'); DebugLogger.log(
'ShareReceiver: failed to parse shared media: $e',
scope: 'share',
);
} }
}); });
@@ -197,11 +203,9 @@ Future<void> _processPayload(Ref ref, SharedPayload payload) async {
// Do NOT create a server chat here. The chat is created on first send // Do NOT create a server chat here. The chat is created on first send
// (with server syncing + title generation) in chat_providers.dart. // (with server syncing + title generation) in chat_providers.dart.
} catch (e) { } catch (e) {
debugPrint('ShareReceiver: failed to process payload: $e'); DebugLogger.log(
'ShareReceiver: failed to process payload: $e',
scope: 'share',
);
} }
} }
void debugPrint(String? message, {int? wrapWidth}) {
if (message == null) return;
DebugLogger.fromLegacy(message, scope: 'share');
}

View File

@@ -2,11 +2,6 @@ import 'package:socket_io_client/socket_io_client.dart' as io;
import '../models/server_config.dart'; import '../models/server_config.dart';
import '../utils/debug_logger.dart'; import '../utils/debug_logger.dart';
void debugPrint(String? message, {int? wrapWidth}) {
if (message == null) return;
DebugLogger.fromLegacy(message, scope: 'socket');
}
class SocketService { class SocketService {
final ServerConfig serverConfig; final ServerConfig serverConfig;
final bool websocketOnly; final bool websocketOnly;
@@ -97,7 +92,7 @@ class SocketService {
_socket = io.io(base, builder.build()); _socket = io.io(base, builder.build());
_socket!.on('connect', (_) { _socket!.on('connect', (_) {
debugPrint('Socket connected: ${_socket!.id}'); DebugLogger.log('Socket connected: ${_socket!.id}', scope: 'socket');
if (_authToken != null && _authToken!.isNotEmpty) { if (_authToken != null && _authToken!.isNotEmpty) {
_socket!.emit('user-join', { _socket!.emit('user-join', {
'auth': {'token': _authToken}, 'auth': {'token': _authToken},
@@ -106,15 +101,18 @@ class SocketService {
}); });
_socket!.on('connect_error', (err) { _socket!.on('connect_error', (err) {
debugPrint('Socket connect_error: $err'); DebugLogger.log('Socket connect_error: $err', scope: 'socket');
}); });
_socket!.on('reconnect_attempt', (attempt) { _socket!.on('reconnect_attempt', (attempt) {
debugPrint('Socket reconnect_attempt: $attempt'); DebugLogger.log('Socket reconnect_attempt: $attempt', scope: 'socket');
}); });
_socket!.on('reconnect', (attempt) { _socket!.on('reconnect', (attempt) {
debugPrint('Socket reconnected after $attempt attempts'); DebugLogger.log(
'Socket reconnected after $attempt attempts',
scope: 'socket',
);
if (_authToken != null && _authToken!.isNotEmpty) { if (_authToken != null && _authToken!.isNotEmpty) {
// Best-effort rejoin // Best-effort rejoin
_socket!.emit('user-join', { _socket!.emit('user-join', {
@@ -124,11 +122,11 @@ class SocketService {
}); });
_socket!.on('reconnect_failed', (_) { _socket!.on('reconnect_failed', (_) {
debugPrint('Socket reconnect_failed'); DebugLogger.log('Socket reconnect_failed', scope: 'socket');
}); });
_socket!.on('disconnect', (reason) { _socket!.on('disconnect', (reason) {
debugPrint('Socket disconnected: $reason'); DebugLogger.log('Socket disconnected: $reason', scope: 'socket');
}); });
} }

View File

@@ -6,11 +6,6 @@ import '../models/conversation.dart';
import 'secure_credential_storage.dart'; import 'secure_credential_storage.dart';
import '../utils/debug_logger.dart'; import '../utils/debug_logger.dart';
void debugPrint(String? message, {int? wrapWidth}) {
if (message == null) return;
DebugLogger.fromLegacy(message, scope: 'storage');
}
class StorageService { class StorageService {
final FlutterSecureStorage _secureStorage; final FlutterSecureStorage _secureStorage;
final SharedPreferences _prefs; final SharedPreferences _prefs;
@@ -42,7 +37,10 @@ class StorageService {
try { try {
await _secureCredentialStorage.saveAuthToken(token); await _secureCredentialStorage.saveAuthToken(token);
} catch (e) { } catch (e) {
debugPrint('Warning: Enhanced secure storage failed, using fallback: $e'); DebugLogger.log(
'Enhanced secure storage failed, using fallback: $e',
scope: 'storage',
);
await _secureStorage.write(key: _authTokenKey, value: token); await _secureStorage.write(key: _authTokenKey, value: token);
} }
} }
@@ -53,7 +51,10 @@ class StorageService {
final token = await _secureCredentialStorage.getAuthToken(); final token = await _secureCredentialStorage.getAuthToken();
if (token != null) return token; if (token != null) return token;
} catch (e) { } catch (e) {
debugPrint('Warning: Enhanced secure storage failed, using fallback: $e'); DebugLogger.log(
'Enhanced secure storage failed, using fallback: $e',
scope: 'storage',
);
} }
// Fallback to legacy storage // Fallback to legacy storage
@@ -65,7 +66,10 @@ class StorageService {
try { try {
await _secureCredentialStorage.deleteAuthToken(); await _secureCredentialStorage.deleteAuthToken();
} catch (e) { } catch (e) {
debugPrint('Warning: Failed to delete from enhanced storage: $e'); DebugLogger.log(
'Failed to delete from enhanced storage: $e',
scope: 'storage',
);
} }
await _secureStorage.delete(key: _authTokenKey); await _secureStorage.delete(key: _authTokenKey);
@@ -83,8 +87,9 @@ class StorageService {
final isSecureAvailable = await _secureCredentialStorage final isSecureAvailable = await _secureCredentialStorage
.isSecureStorageAvailable(); .isSecureStorageAvailable();
if (!isSecureAvailable) { if (!isSecureAvailable) {
debugPrint( DebugLogger.log(
'DEBUG: Enhanced secure storage not available, using legacy storage', 'Enhanced secure storage not available, using legacy storage',
scope: 'storage',
); );
throw Exception('Enhanced secure storage not available'); throw Exception('Enhanced secure storage not available');
} }
@@ -94,9 +99,15 @@ class StorageService {
username: username, username: username,
password: password, password: password,
); );
debugPrint('DEBUG: Credentials saved using enhanced secure storage'); DebugLogger.log(
'Credentials saved using enhanced secure storage',
scope: 'storage',
);
} catch (e) { } catch (e) {
debugPrint('Warning: Enhanced secure storage failed, using fallback: $e'); DebugLogger.log(
'Enhanced secure storage failed, using fallback: $e',
scope: 'storage',
);
// Fallback to legacy storage // Fallback to legacy storage
try { try {
@@ -120,10 +131,14 @@ class StorageService {
); );
} }
debugPrint('DEBUG: Credentials saved using fallback storage'); DebugLogger.log(
'Credentials saved using fallback storage',
scope: 'storage',
);
} catch (fallbackError) { } catch (fallbackError) {
debugPrint( DebugLogger.log(
'ERROR: Both enhanced and fallback credential storage failed: $fallbackError', 'Both enhanced and fallback credential storage failed: $fallbackError',
scope: 'storage',
); );
rethrow; rethrow;
} }
@@ -138,7 +153,10 @@ class StorageService {
return credentials; return credentials;
} }
} catch (e) { } catch (e) {
debugPrint('Warning: Enhanced secure storage failed, using fallback: $e'); DebugLogger.log(
'Enhanced secure storage failed, using fallback: $e',
scope: 'storage',
);
} }
// Fallback to legacy storage and migrate if found // Fallback to legacy storage and migrate if found
@@ -153,7 +171,7 @@ class StorageService {
if (!decoded.containsKey('serverId') || if (!decoded.containsKey('serverId') ||
!decoded.containsKey('username') || !decoded.containsKey('username') ||
!decoded.containsKey('password')) { !decoded.containsKey('password')) {
debugPrint('Warning: Invalid saved credentials format'); DebugLogger.log('Invalid saved credentials format', scope: 'storage');
await deleteSavedCredentials(); await deleteSavedCredentials();
return null; return null;
} }
@@ -170,16 +188,17 @@ class StorageService {
await _secureCredentialStorage.migrateFromOldStorage(legacyCredentials); await _secureCredentialStorage.migrateFromOldStorage(legacyCredentials);
// If migration successful, clean up legacy storage // If migration successful, clean up legacy storage
await _secureStorage.delete(key: _credentialsKey); await _secureStorage.delete(key: _credentialsKey);
debugPrint( DebugLogger.log(
'DEBUG: Successfully migrated credentials to enhanced storage', 'Successfully migrated credentials to enhanced storage',
scope: 'storage',
); );
} catch (e) { } catch (e) {
debugPrint('Warning: Failed to migrate credentials: $e'); DebugLogger.log('Failed to migrate credentials: $e', scope: 'storage');
} }
return legacyCredentials; return legacyCredentials;
} catch (e) { } catch (e) {
debugPrint('Error loading saved credentials: $e'); DebugLogger.log('Error loading saved credentials: $e', scope: 'storage');
return null; return null;
} }
} }
@@ -189,7 +208,10 @@ class StorageService {
try { try {
await _secureCredentialStorage.deleteSavedCredentials(); await _secureCredentialStorage.deleteSavedCredentials();
} catch (e) { } catch (e) {
debugPrint('Warning: Failed to delete from enhanced storage: $e'); DebugLogger.log(
'Failed to delete from enhanced storage: $e',
scope: 'storage',
);
} }
await _secureStorage.delete(key: _credentialsKey); await _secureStorage.delete(key: _credentialsKey);
@@ -218,7 +240,10 @@ class StorageService {
final decoded = jsonDecode(jsonString); final decoded = jsonDecode(jsonString);
if (decoded is! List) { if (decoded is! List) {
debugPrint('Warning: Server configs data is not a list, resetting'); DebugLogger.log(
'Server configs data is not a list, resetting',
scope: 'storage',
);
return []; return [];
} }
@@ -232,20 +257,24 @@ class StorageService {
item.containsKey('url')) { item.containsKey('url')) {
configs.add(ServerConfig.fromJson(item)); configs.add(ServerConfig.fromJson(item));
} else { } else {
debugPrint( DebugLogger.log(
'Warning: Skipping invalid server config: missing required fields', 'Skipping invalid server config: missing required fields',
scope: 'storage',
); );
} }
} }
} catch (e) { } catch (e) {
debugPrint('Warning: Failed to parse server config: $e'); DebugLogger.log(
'Failed to parse server config: $e',
scope: 'storage',
);
// Continue with other configs // Continue with other configs
} }
} }
return configs; return configs;
} catch (e) { } catch (e) {
debugPrint('Error loading server configs: $e'); DebugLogger.log('Error loading server configs: $e', scope: 'storage');
return []; return [];
} }
} }
@@ -279,8 +308,9 @@ class StorageService {
try { try {
final decoded = jsonDecode(jsonString); final decoded = jsonDecode(jsonString);
if (decoded is! List) { if (decoded is! List) {
debugPrint( DebugLogger.log(
'Warning: Local conversations data is not a list, resetting', 'Local conversations data is not a list, resetting',
scope: 'storage',
); );
return []; return [];
} }
@@ -296,20 +326,24 @@ class StorageService {
item.containsKey('updatedAt')) { item.containsKey('updatedAt')) {
conversations.add(Conversation.fromJson(item)); conversations.add(Conversation.fromJson(item));
} else { } else {
debugPrint( DebugLogger.log(
'Warning: Skipping invalid conversation: missing required fields', 'Skipping invalid conversation: missing required fields',
scope: 'storage',
); );
} }
} }
} catch (e) { } catch (e) {
debugPrint('Warning: Failed to parse conversation: $e'); DebugLogger.log('Failed to parse conversation: $e', scope: 'storage');
// Continue with other conversations // Continue with other conversations
} }
} }
return conversations; return conversations;
} catch (e) { } catch (e) {
debugPrint('Error parsing local conversations: $e'); DebugLogger.log(
'Error parsing local conversations: $e',
scope: 'storage',
);
return []; return [];
} }
} }
@@ -319,7 +353,7 @@ class StorageService {
final json = conversations.map((c) => c.toJson()).toList(); final json = conversations.map((c) => c.toJson()).toList();
await _prefs.setString(_localConversationsKey, jsonEncode(json)); await _prefs.setString(_localConversationsKey, jsonEncode(json));
} catch (e) { } catch (e) {
debugPrint('Error saving local conversations: $e'); DebugLogger.log('Error saving local conversations: $e', scope: 'storage');
} }
} }
@@ -350,21 +384,21 @@ class StorageService {
try { try {
await _secureCredentialStorage.clearAll(); await _secureCredentialStorage.clearAll();
} catch (e) { } catch (e) {
debugPrint('Warning: Failed to clear enhanced storage: $e'); DebugLogger.log('Failed to clear enhanced storage: $e', scope: 'storage');
} }
// Clear legacy storage // Clear legacy storage
await _secureStorage.deleteAll(); await _secureStorage.deleteAll();
await _prefs.clear(); await _prefs.clear();
debugPrint('DEBUG: All storage cleared'); DebugLogger.log('All storage cleared', scope: 'storage');
} }
// Clear only auth-related data (keeping server configs and other settings) // Clear only auth-related data (keeping server configs and other settings)
Future<void> clearAuthData() async { Future<void> clearAuthData() async {
await deleteAuthToken(); await deleteAuthToken();
await deleteSavedCredentials(); await deleteSavedCredentials();
debugPrint('DEBUG: Auth data cleared'); DebugLogger.log('Auth data cleared', scope: 'storage');
} }
/// Check if enhanced secure storage is available /// Check if enhanced secure storage is available
@@ -372,7 +406,10 @@ class StorageService {
try { try {
return await _secureCredentialStorage.isSecureStorageAvailable(); return await _secureCredentialStorage.isSecureStorageAvailable();
} catch (e) { } catch (e) {
debugPrint('Warning: Failed to check enhanced storage availability: $e'); DebugLogger.log(
'Failed to check enhanced storage availability: $e',
scope: 'storage',
);
return false; return false;
} }
} }

View File

@@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart' hide debugPrint; import 'package:flutter/material.dart';
import '../../core/models/chat_message.dart'; import '../../core/models/chat_message.dart';
import '../../core/services/persistent_streaming_service.dart'; import '../../core/services/persistent_streaming_service.dart';
@@ -14,11 +14,6 @@ import '../../shared/widgets/themed_dialogs.dart';
import '../../shared/theme/theme_extensions.dart'; import '../../shared/theme/theme_extensions.dart';
import '../utils/debug_logger.dart'; import '../utils/debug_logger.dart';
void debugPrint(String? message, {int? wrapWidth}) {
if (message == null) return;
DebugLogger.fromLegacy(message, scope: 'streaming/helper');
}
// Keep local verbosity toggle for socket logs // Keep local verbosity toggle for socket logs
const bool kSocketVerboseLogging = false; const bool kSocketVerboseLogging = false;
@@ -83,7 +78,10 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
), ),
controller: persistentController, controller: persistentController,
recoveryCallback: () async { recoveryCallback: () async {
debugPrint('DEBUG: Attempting to recover interrupted stream'); DebugLogger.log(
'Attempting to recover interrupted stream',
scope: 'streaming/helper',
);
}, },
metadata: { metadata: {
'conversationId': activeConversationId, 'conversationId': activeConversationId,

View File

@@ -1,15 +1,10 @@
import 'package:flutter/foundation.dart' hide debugPrint; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:conduit/l10n/app_localizations.dart'; import 'package:conduit/l10n/app_localizations.dart';
import '../../shared/theme/theme_extensions.dart'; import '../../shared/theme/theme_extensions.dart';
import 'navigation_service.dart'; import 'navigation_service.dart';
import '../utils/debug_logger.dart'; import '../utils/debug_logger.dart';
void debugPrint(String? message, {int? wrapWidth}) {
if (message == null) return;
DebugLogger.fromLegacy(message, scope: 'errors/user-friendly');
}
/// User-friendly error messages and recovery actions /// User-friendly error messages and recovery actions
class UserFriendlyErrorHandler { class UserFriendlyErrorHandler {
static final UserFriendlyErrorHandler _instance = static final UserFriendlyErrorHandler _instance =
@@ -347,9 +342,12 @@ class UserFriendlyErrorHandler {
/// Log technical error details for debugging /// Log technical error details for debugging
void _logError(dynamic error) { void _logError(dynamic error) {
if (kDebugMode) { if (kDebugMode) {
debugPrint('ERROR: $error'); DebugLogger.log('$error', scope: 'errors/user-friendly');
if (error is Error) { if (error is Error) {
debugPrint('STACK TRACE: ${error.stackTrace}'); DebugLogger.log(
'STACK TRACE: ${error.stackTrace}',
scope: 'errors/user-friendly',
);
} }
} }

View File

@@ -16,11 +16,6 @@ import '../../tools/providers/tools_providers.dart';
import 'dart:async'; import 'dart:async';
import '../../../core/utils/debug_logger.dart'; import '../../../core/utils/debug_logger.dart';
void debugPrint(String? message, {int? wrapWidth}) {
if (message == null) return;
DebugLogger.fromLegacy(message, scope: 'chat/providers');
}
const bool kSocketVerboseLogging = false; const bool kSocketVerboseLogging = false;
// Chat messages for current conversation // Chat messages for current conversation
@@ -105,7 +100,10 @@ class ChatMessagesNotifier extends Notifier<List<ChatMessage>> {
previous, previous,
next, next,
) { ) {
debugPrint('Conversation changed: ${previous?.id} -> ${next?.id}'); DebugLogger.log(
'Conversation changed: ${previous?.id} -> ${next?.id}',
scope: 'chat/providers',
);
// Only react when the conversation actually changes // Only react when the conversation actually changes
if (previous?.id == next?.id) { if (previous?.id == next?.id) {
@@ -1886,7 +1884,10 @@ Please try sending the message again, or try without attachments.''',
); );
ref.read(chatMessagesProvider.notifier).addMessage(errorMessage); ref.read(chatMessagesProvider.notifier).addMessage(errorMessage);
} else if (e.toString().contains('404')) { } else if (e.toString().contains('404')) {
debugPrint('DEBUG: Model or endpoint not found (404)'); DebugLogger.log(
'Model or endpoint not found (404)',
scope: 'chat/providers',
);
final errorMessage = ChatMessage( final errorMessage = ChatMessage(
id: const Uuid().v4(), id: const Uuid().v4(),
role: 'assistant', role: 'assistant',
@@ -2005,7 +2006,10 @@ Future<void> pinConversation(
.set(activeConversation!.copyWith(pinned: pinned)); .set(activeConversation!.copyWith(pinned: pinned));
} }
} catch (e) { } catch (e) {
debugPrint('Error ${pinned ? 'pinning' : 'unpinning'} conversation: $e'); DebugLogger.log(
'Error ${pinned ? 'pinning' : 'unpinning'} conversation: $e',
scope: 'chat/providers',
);
rethrow; rethrow;
} }
} }
@@ -2033,8 +2037,9 @@ Future<void> archiveConversation(
// Refresh conversations list to reflect the change // Refresh conversations list to reflect the change
ref.invalidate(conversationsProvider); ref.invalidate(conversationsProvider);
} catch (e) { } catch (e) {
debugPrint( DebugLogger.log(
'Error ${archived ? 'archiving' : 'unarchiving'} conversation: $e', 'Error ${archived ? 'archiving' : 'unarchiving'} conversation: $e',
scope: 'chat/providers',
); );
// If server operation failed and we archived locally, restore the conversation // If server operation failed and we archived locally, restore the conversation
@@ -2060,7 +2065,7 @@ Future<String?> shareConversation(WidgetRef ref, String conversationId) async {
return shareId; return shareId;
} catch (e) { } catch (e) {
debugPrint('Error sharing conversation: $e'); DebugLogger.log('Error sharing conversation: $e', scope: 'chat/providers');
rethrow; rethrow;
} }
} }
@@ -2081,7 +2086,7 @@ Future<void> cloneConversation(WidgetRef ref, String conversationId) async {
// Refresh conversations list to show the new conversation // Refresh conversations list to show the new conversation
ref.invalidate(conversationsProvider); ref.invalidate(conversationsProvider);
} catch (e) { } catch (e) {
debugPrint('Error cloning conversation: $e'); DebugLogger.log('Error cloning conversation: $e', scope: 'chat/providers');
rethrow; rethrow;
} }
} }

View File

@@ -1,4 +1,4 @@
import 'package:flutter/material.dart' hide debugPrint; import 'package:flutter/material.dart';
import 'package:conduit/l10n/app_localizations.dart'; import 'package:conduit/l10n/app_localizations.dart';
import '../../../core/widgets/error_boundary.dart'; import '../../../core/widgets/error_boundary.dart';
import '../../../shared/widgets/optimized_list.dart'; import '../../../shared/widgets/optimized_list.dart';
@@ -48,11 +48,6 @@ import '../../../shared/widgets/model_avatar.dart';
import '../../../core/services/platform_service.dart' as ps; import '../../../core/services/platform_service.dart' as ps;
import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/gestures.dart' show DragStartBehavior;
void debugPrint(String? message, {int? wrapWidth}) {
if (message == null) return;
DebugLogger.fromLegacy(message, scope: 'chat/page');
}
class ChatPage extends ConsumerStatefulWidget { class ChatPage extends ConsumerStatefulWidget {
const ChatPage({super.key}); const ChatPage({super.key});
@@ -251,7 +246,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
if (!mounted) return; if (!mounted) return;
ref.read(activeConversationProvider.notifier).set(welcomeConv); ref.read(activeConversationProvider.notifier).set(welcomeConv);
debugPrint('Auto-loaded demo conversation'); DebugLogger.log('Auto-loaded demo conversation', scope: 'chat/page');
return; return;
} }
@@ -266,7 +261,10 @@ class _ChatPageState extends ConsumerState<ChatPage> {
break; break;
} }
debugPrint('Failed to auto-load demo conversation'); DebugLogger.log(
'Failed to auto-load demo conversation',
scope: 'chat/page',
);
} }
@override @override
@@ -439,18 +437,19 @@ class _ChatPageState extends ConsumerState<ChatPage> {
); );
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
debugPrint('Enqueue upload failed: $e'); DebugLogger.log('Enqueue upload failed: $e', scope: 'chat/page');
} }
} }
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
debugPrint('File selection failed: $e'); DebugLogger.log('File selection failed: $e', scope: 'chat/page');
} }
} }
void _handleImageAttachment({bool fromCamera = false}) async { void _handleImageAttachment({bool fromCamera = false}) async {
debugPrint( DebugLogger.log(
'DEBUG: Starting image attachment process - fromCamera: $fromCamera', 'Starting image attachment process - fromCamera: $fromCamera',
scope: 'chat/page',
); );
// Check if selected model supports vision // Check if selected model supports vision
@@ -462,23 +461,26 @@ class _ChatPageState extends ConsumerState<ChatPage> {
final fileService = ref.read(fileAttachmentServiceProvider); final fileService = ref.read(fileAttachmentServiceProvider);
if (fileService == null) { if (fileService == null) {
debugPrint('DEBUG: File service is null - cannot proceed'); DebugLogger.log(
'File service is null - cannot proceed',
scope: 'chat/page',
);
return; return;
} }
try { try {
debugPrint('DEBUG: Picking image...'); DebugLogger.log('Picking image...', scope: 'chat/page');
final image = fromCamera final image = fromCamera
? await fileService.takePhoto() ? await fileService.takePhoto()
: await fileService.pickImage(); : await fileService.pickImage();
if (image == null) { if (image == null) {
debugPrint('DEBUG: No image selected'); DebugLogger.log('No image selected', scope: 'chat/page');
return; return;
} }
debugPrint('DEBUG: Image selected: ${image.path}'); DebugLogger.log('Image selected: ${image.path}', scope: 'chat/page');
final imageSize = await image.length(); final imageSize = await image.length();
debugPrint('DEBUG: Image size: $imageSize bytes'); DebugLogger.log('Image size: $imageSize bytes', scope: 'chat/page');
// Validate file size (default 20MB limit like OpenWebUI) // Validate file size (default 20MB limit like OpenWebUI)
if (!validateFileSize(imageSize, 20)) { if (!validateFileSize(imageSize, 20)) {
@@ -495,10 +497,10 @@ class _ChatPageState extends ConsumerState<ChatPage> {
// Add image to the attachment list // Add image to the attachment list
ref.read(attachedFilesProvider.notifier).addFiles([image]); ref.read(attachedFilesProvider.notifier).addFiles([image]);
debugPrint('DEBUG: Image added to attachment list'); DebugLogger.log('Image added to attachment list', scope: 'chat/page');
// Enqueue upload via task queue for unified retry/progress // Enqueue upload via task queue for unified retry/progress
debugPrint('DEBUG: Enqueueing image upload...'); DebugLogger.log('Enqueueing image upload...', scope: 'chat/page');
final activeConv = ref.read(activeConversationProvider); final activeConv = ref.read(activeConversationProvider);
try { try {
await ref await ref
@@ -510,10 +512,10 @@ class _ChatPageState extends ConsumerState<ChatPage> {
fileSize: imageSize, fileSize: imageSize,
); );
} catch (e) { } catch (e) {
debugPrint('DEBUG: Enqueue image upload failed: $e'); DebugLogger.log('Enqueue image upload failed: $e', scope: 'chat/page');
} }
} catch (e) { } catch (e) {
debugPrint('DEBUG: Image attachment error: $e'); DebugLogger.log('Image attachment error: $e', scope: 'chat/page');
if (!mounted) return; if (!mounted) return;
} }
} }
@@ -886,7 +888,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
userMessage.attachmentIds, userMessage.attachmentIds,
); );
} catch (e) { } catch (e) {
debugPrint('Regenerate failed: $e'); DebugLogger.log('Regenerate failed: $e', scope: 'chat/page');
} }
} }
@@ -1389,8 +1391,9 @@ class _ChatPageState extends ConsumerState<ChatPage> {
.read(activeConversationProvider.notifier) .read(activeConversationProvider.notifier)
.set(full); .set(full);
} catch (e) { } catch (e) {
debugPrint( DebugLogger.log(
'DEBUG: Failed to refresh conversation: $e', 'Failed to refresh conversation: $e',
scope: 'chat/page',
); );
} }
} }
@@ -2101,7 +2104,7 @@ class _VoiceInputSheetState extends ConsumerState<_VoiceInputSheet> {
}); });
}, },
onDone: () { onDone: () {
debugPrint('DEBUG: VoiceInputSheet stream done'); DebugLogger.log('VoiceInputSheet stream done', scope: 'chat/page');
setState(() { setState(() {
_isListening = false; _isListening = false;
}); });
@@ -2112,7 +2115,10 @@ class _VoiceInputSheetState extends ConsumerState<_VoiceInputSheet> {
} }
}, },
onError: (error) { onError: (error) {
debugPrint('DEBUG: VoiceInputSheet stream error: $error'); DebugLogger.log(
'VoiceInputSheet stream error: $error',
scope: 'chat/page',
);
setState(() { setState(() {
_isListening = false; _isListening = false;
}); });

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart' hide debugPrint; import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart' show listEquals;
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'dart:convert'; import 'dart:convert';
@@ -22,11 +21,6 @@ import 'package:url_launcher/url_launcher_string.dart';
import '../providers/chat_providers.dart' show sendMessage; import '../providers/chat_providers.dart' show sendMessage;
import '../../../core/utils/debug_logger.dart'; import '../../../core/utils/debug_logger.dart';
void debugPrint(String? message, {int? wrapWidth}) {
if (message == null) return;
DebugLogger.fromLegacy(message, scope: 'chat/assistant');
}
class AssistantMessageWidget extends ConsumerStatefulWidget { class AssistantMessageWidget extends ConsumerStatefulWidget {
final dynamic message; final dynamic message;
final bool isStreaming; final bool isStreaming;
@@ -76,7 +70,10 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
try { try {
await sendMessage(ref, trimmed, null); await sendMessage(ref, trimmed, null);
} catch (err, stack) { } catch (err, stack) {
debugPrint('Failed to send follow-up: $err'); DebugLogger.log(
'Failed to send follow-up: $err',
scope: 'chat/assistant',
);
debugPrintStack(stackTrace: stack); debugPrintStack(stackTrace: stack);
} }
} }
@@ -660,15 +657,6 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
const SizedBox(height: Spacing.md), const SizedBox(height: Spacing.md),
CitationListView(sources: widget.message.sources), CitationListView(sources: widget.message.sources),
], ],
if (hasFollowUps) ...[
const SizedBox(height: Spacing.md),
FollowUpSuggestionBar(
suggestions: widget.message.followUps,
onSelected: _handleFollowUpTap,
isBusy: widget.isStreaming,
),
],
], ],
), ),
), ),
@@ -677,6 +665,14 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
if (!widget.isStreaming) ...[ if (!widget.isStreaming) ...[
const SizedBox(height: Spacing.sm), const SizedBox(height: Spacing.sm),
_buildActionButtons(), _buildActionButtons(),
if (hasFollowUps) ...[
const SizedBox(height: Spacing.md),
FollowUpSuggestionBar(
suggestions: widget.message.followUps,
onSelected: _handleFollowUpTap,
isBusy: widget.isStreaming,
),
],
], ],
], ],
), ),
@@ -1283,6 +1279,124 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
} }
} }
class _AssistantResponseSection extends StatelessWidget {
const _AssistantResponseSection({
required this.title,
required this.child,
this.icon,
});
final String title;
final Widget child;
final IconData? icon;
@override
Widget build(BuildContext context) {
final theme = context.conduitTheme;
final colorScheme = Theme.of(context).colorScheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (icon != null) ...[
Icon(icon, size: 16, color: theme.buttonPrimary),
const SizedBox(width: Spacing.xs),
],
Text(
title,
style: TextStyle(
color: theme.textSecondary,
fontSize: AppTypography.bodySmall,
fontWeight: FontWeight.w600,
letterSpacing: 0.15,
),
),
],
),
const SizedBox(height: Spacing.xs),
Container(
width: double.infinity,
padding: const EdgeInsets.all(Spacing.sm),
decoration: BoxDecoration(
color: theme.cardBackground,
borderRadius: BorderRadius.circular(AppBorderRadius.card),
border: Border.all(
color: theme.cardBorder.withValues(alpha: 0.6),
width: BorderWidth.thin,
),
boxShadow: [
BoxShadow(
color: colorScheme.shadow.withValues(alpha: 0.05),
blurRadius: 16,
offset: const Offset(0, 6),
),
],
),
child: child,
),
],
);
}
}
class _AssistantSuggestionChip extends StatelessWidget {
const _AssistantSuggestionChip({
required this.label,
this.icon,
this.onPressed,
this.enabled = true,
});
final String label;
final IconData? icon;
final VoidCallback? onPressed;
final bool enabled;
@override
Widget build(BuildContext context) {
final theme = context.conduitTheme;
final effectiveOnPressed = enabled ? onPressed : null;
final iconColor = enabled
? theme.textSecondary
: theme.textSecondary.withValues(alpha: 0.5);
final background = theme.cardBackground.withValues(
alpha: enabled ? 0.95 : 0.85,
);
final borderColor = theme.cardBorder.withValues(
alpha: enabled ? 0.6 : 0.35,
);
return RawChip(
avatar: icon != null ? Icon(icon, size: 16, color: iconColor) : null,
label: Text(
label,
style: TextStyle(
color: enabled ? theme.textPrimary : theme.textSecondary,
fontSize: AppTypography.labelMedium,
fontWeight: FontWeight.w500,
letterSpacing: 0.2,
),
),
onPressed: effectiveOnPressed,
padding: const EdgeInsets.symmetric(
horizontal: Spacing.sm,
vertical: Spacing.xxs,
),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity.compact,
backgroundColor: background,
disabledColor: background,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.pill),
side: BorderSide(color: borderColor, width: BorderWidth.thin),
),
);
}
}
class StatusHistoryTimeline extends StatelessWidget { class StatusHistoryTimeline extends StatelessWidget {
const StatusHistoryTimeline({super.key, required this.updates}); const StatusHistoryTimeline({super.key, required this.updates});
@@ -1290,39 +1404,24 @@ class StatusHistoryTimeline extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = context.conduitTheme;
if (updates.isEmpty) { if (updates.isEmpty) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return Container( return _AssistantResponseSection(
width: double.infinity, title: 'Status updates',
padding: const EdgeInsets.all(Spacing.sm), icon: Icons.sync_alt,
decoration: BoxDecoration(
color: theme.surfaceContainer.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(AppBorderRadius.md),
border: Border.all(
color: theme.dividerColor.withValues(alpha: 0.6),
width: BorderWidth.thin,
),
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( const SizedBox(height: Spacing.xs),
'Status updates', for (var index = 0; index < updates.length; index++)
style: TextStyle( Padding(
color: theme.textPrimary, padding: EdgeInsets.only(
fontWeight: FontWeight.w600, bottom: index == updates.length - 1 ? 0 : Spacing.xs,
fontSize: AppTypography.bodyLarge, ),
child: _StatusHistoryEntry(update: updates[index]),
), ),
),
const SizedBox(height: Spacing.sm),
...List.generate(updates.length, (index) {
final update = updates[index];
final isLast = index == updates.length - 1;
return _StatusHistoryEntry(update: update, isLast: isLast);
}),
], ],
), ),
); );
@@ -1330,10 +1429,9 @@ class StatusHistoryTimeline extends StatelessWidget {
} }
class _StatusHistoryEntry extends StatelessWidget { class _StatusHistoryEntry extends StatelessWidget {
const _StatusHistoryEntry({required this.update, required this.isLast}); const _StatusHistoryEntry({required this.update});
final ChatStatusUpdate update; final ChatStatusUpdate update;
final bool isLast;
Color _indicatorColor(ConduitThemeExtension theme) { Color _indicatorColor(ConduitThemeExtension theme) {
if (update.done == false) { if (update.done == false) {
@@ -1372,142 +1470,159 @@ class _StatusHistoryEntry extends StatelessWidget {
} }
} }
return Padding( return Container(
padding: const EdgeInsets.only(bottom: Spacing.sm), width: double.infinity,
child: Row( padding: const EdgeInsets.symmetric(
horizontal: Spacing.sm,
vertical: Spacing.sm,
),
decoration: BoxDecoration(
color: theme.cardBackground.withValues(alpha: 0.92),
borderRadius: BorderRadius.circular(AppBorderRadius.md),
border: Border.all(
color: theme.cardBorder.withValues(alpha: 0.5),
width: BorderWidth.thin,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Column( Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Icon(_indicatorIcon(), size: 18, color: indicatorColor), Icon(_indicatorIcon(), size: 16, color: indicatorColor),
if (!isLast) const SizedBox(width: Spacing.sm),
Container( Expanded(
margin: const EdgeInsets.only(top: Spacing.xxs), child: Column(
width: 2, crossAxisAlignment: CrossAxisAlignment.start,
height: 32, children: [
color: theme.dividerColor.withValues(alpha: 0.5), Text(
description,
style: TextStyle(
fontSize: AppTypography.bodySmall,
color: theme.textSecondary,
fontWeight: update.done == true
? FontWeight.w600
: FontWeight.w500,
),
),
if (update.count != null)
Padding(
padding: const EdgeInsets.only(top: Spacing.xxs),
child: Text(
update.count == 1
? 'Retrieved 1 source'
: 'Retrieved ${update.count} sources',
style: TextStyle(
color: theme.textSecondary,
fontSize: AppTypography.labelSmall,
fontWeight: FontWeight.w500,
),
),
),
if (timestamp != null)
Padding(
padding: const EdgeInsets.only(top: Spacing.xxs),
child: Text(
_formatTimestamp(timestamp),
style: TextStyle(
color: theme.textSecondary.withValues(alpha: 0.8),
fontSize: AppTypography.labelSmall,
),
),
),
],
), ),
),
], ],
), ),
const SizedBox(width: Spacing.sm), if (queries.isNotEmpty)
Expanded( Padding(
child: Column( padding: const EdgeInsets.only(top: Spacing.sm),
crossAxisAlignment: CrossAxisAlignment.start, child: Wrap(
children: [ spacing: Spacing.xs,
Text( runSpacing: Spacing.xs,
description, children: queries.map((query) {
style: TextStyle( return _AssistantSuggestionChip(
fontSize: AppTypography.bodyMedium, label: query,
color: theme.textPrimary, icon: Icons.search,
fontWeight: update.done == true onPressed: () {
? FontWeight.w600 _launchUri(
: FontWeight.w500, 'https://www.google.com/search?q=${Uri.encodeComponent(query)}',
), );
), },
if (update.count != null) );
Padding( }).toList(),
padding: const EdgeInsets.only(top: Spacing.xxs), ),
child: Text( ),
update.count == 1 if (update.urls.isNotEmpty)
? 'Retrieved 1 source' Padding(
: 'Retrieved ${update.count} sources', padding: const EdgeInsets.only(top: Spacing.sm),
style: TextStyle( child: Wrap(
color: theme.textSecondary, spacing: Spacing.xs,
fontSize: AppTypography.labelSmall, runSpacing: Spacing.xs,
), children: update.urls.map((url) {
), final host = Uri.tryParse(url)?.host ?? 'Link';
), return _AssistantSuggestionChip(
if (timestamp != null) label: host,
Padding( icon: Icons.open_in_new,
padding: const EdgeInsets.only(top: Spacing.xxs), onPressed: () => _launchUri(url),
child: Text( );
_formatTimestamp(timestamp), }).toList(),
style: TextStyle( ),
color: theme.textSecondary, ),
fontSize: AppTypography.labelSmall, if (update.items.isNotEmpty)
), Padding(
), padding: const EdgeInsets.only(top: Spacing.sm),
), child: Column(
if (queries.isNotEmpty) crossAxisAlignment: CrossAxisAlignment.start,
Padding( children: update.items.map((item) {
padding: const EdgeInsets.only(top: Spacing.xxs), final title = item.title?.isNotEmpty == true
child: Wrap( ? item.title!
spacing: Spacing.xs, : item.link ?? 'Result';
runSpacing: Spacing.xs, return Padding(
children: queries.map((query) { padding: const EdgeInsets.only(bottom: Spacing.xs),
return ActionChip( child: InkWell(
label: Text(query), onTap: item.link != null
avatar: const Icon(Icons.search, size: 16), ? () => _launchUri(item.link!)
onPressed: () { : null,
_launchUri( borderRadius: BorderRadius.circular(AppBorderRadius.sm),
'https://www.google.com/search?q=${Uri.encodeComponent(query)}', child: Padding(
); padding: const EdgeInsets.symmetric(
}, vertical: Spacing.xxs,
); ),
}).toList(), child: Row(
), crossAxisAlignment: CrossAxisAlignment.start,
), children: [
if (update.urls.isNotEmpty) Icon(
Padding( Icons.link,
padding: const EdgeInsets.only(top: Spacing.xxs), size: 16,
child: Wrap( color: theme.textSecondary,
spacing: Spacing.xs, ),
runSpacing: Spacing.xs, const SizedBox(width: Spacing.xs),
children: update.urls.map((url) { Expanded(
return OutlinedButton.icon( child: Text(
onPressed: () => _launchUri(url), title,
icon: const Icon(Icons.open_in_new, size: 16), style: TextStyle(
label: Text( color: item.link != null
Uri.tryParse(url)?.host ?? 'Link', ? theme.buttonPrimary
overflow: TextOverflow.ellipsis, : theme.textSecondary,
), decoration: item.link != null
); ? TextDecoration.underline
}).toList(), : TextDecoration.none,
), fontSize: AppTypography.bodySmall,
), fontWeight: FontWeight.w500,
if (update.items.isNotEmpty) ),
Padding( ),
padding: const EdgeInsets.only(top: Spacing.xxs), ),
child: Column( ],
crossAxisAlignment: CrossAxisAlignment.start, ),
children: update.items.map((item) { ),
final title = item.title?.isNotEmpty == true ),
? item.title! );
: item.link ?? 'Result'; }).toList(),
return Padding( ),
padding: const EdgeInsets.only(bottom: Spacing.xxs),
child: InkWell(
onTap: item.link != null
? () => _launchUri(item.link!)
: null,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.link, size: 16),
const SizedBox(width: Spacing.xxs),
Expanded(
child: Text(
title,
style: TextStyle(
color: item.link != null
? theme.buttonPrimary
: theme.textSecondary,
decoration: item.link != null
? TextDecoration.underline
: TextDecoration.none,
),
),
),
],
),
),
);
}).toList(),
),
),
],
), ),
),
], ],
), ),
); );
@@ -1772,7 +1887,7 @@ class CitationListView extends StatelessWidget {
} }
} }
class FollowUpSuggestionBar extends StatefulWidget { class FollowUpSuggestionBar extends StatelessWidget {
const FollowUpSuggestionBar({ const FollowUpSuggestionBar({
super.key, super.key,
required this.suggestions, required this.suggestions,
@@ -1784,149 +1899,37 @@ class FollowUpSuggestionBar extends StatefulWidget {
final ValueChanged<String> onSelected; final ValueChanged<String> onSelected;
final bool isBusy; final bool isBusy;
@override
State<FollowUpSuggestionBar> createState() => _FollowUpSuggestionBarState();
}
class _FollowUpSuggestionBarState extends State<FollowUpSuggestionBar>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 520),
);
if (widget.suggestions.isNotEmpty) {
_controller.forward();
}
}
@override
void didUpdateWidget(covariant FollowUpSuggestionBar oldWidget) {
super.didUpdateWidget(oldWidget);
if (!listEquals(oldWidget.suggestions, widget.suggestions)) {
if (widget.suggestions.isEmpty) {
_controller.reset();
} else {
_controller.forward(from: 0);
}
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = context.conduitTheme; final trimmedSuggestions = suggestions
if (widget.suggestions.isEmpty) { .map((s) => s.trim())
.where((s) => s.isNotEmpty)
.toList(growable: false);
if (trimmedSuggestions.isEmpty) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final Animation<double> headerAnimation = CurvedAnimation( return _AssistantResponseSection(
parent: _controller, title: 'Suggested next steps',
curve: const Interval(0, 0.35, curve: Curves.easeOutCubic), icon: Icons.auto_awesome,
); child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
return Column( children: [
crossAxisAlignment: CrossAxisAlignment.start, const SizedBox(height: Spacing.xs),
children: [ Wrap(
AnimatedBuilder( spacing: Spacing.xs,
animation: headerAnimation, runSpacing: Spacing.xs,
builder: (context, child) { children: [
return Opacity( for (final suggestion in trimmedSuggestions)
opacity: headerAnimation.value, _AssistantSuggestionChip(
child: Transform.translate( label: suggestion,
offset: Offset(0, (1 - headerAnimation.value) * 10), onPressed: isBusy ? null : () => onSelected(suggestion),
child: child, enabled: !isBusy,
), ),
); ],
},
child: Text(
'Try next',
style: TextStyle(
color: theme.textPrimary,
fontWeight: FontWeight.w600,
fontSize: AppTypography.bodyLarge,
),
), ),
), ],
const SizedBox(height: Spacing.xs),
Wrap(
spacing: Spacing.xs,
runSpacing: Spacing.xs,
children: [
for (var i = 0; i < widget.suggestions.length; i++)
_AnimatedSuggestionChip(
controller: _controller,
index: i,
total: widget.suggestions.length,
isBusy: widget.isBusy,
suggestion: widget.suggestions[i],
onSelected: widget.onSelected,
),
],
),
],
);
}
}
class _AnimatedSuggestionChip extends StatelessWidget {
const _AnimatedSuggestionChip({
required this.controller,
required this.index,
required this.total,
required this.isBusy,
required this.suggestion,
required this.onSelected,
});
final AnimationController controller;
final int index;
final int total;
final bool isBusy;
final String suggestion;
final ValueChanged<String> onSelected;
Interval _intervalForIndex() {
if (total <= 1) {
return const Interval(0.0, 0.8, curve: Curves.easeOutCubic);
}
final double step = 0.6 / total;
final double start = (index * step).clamp(0.0, 0.8);
final double end = (start + 0.4).clamp(0.2, 1.0);
return Interval(start, end, curve: Curves.easeOutCubic);
}
@override
Widget build(BuildContext context) {
final animation = CurvedAnimation(
parent: controller,
curve: _intervalForIndex(),
);
return AnimatedBuilder(
animation: animation,
builder: (context, child) {
final double t = animation.value;
return Opacity(
opacity: t,
child: Transform.translate(
offset: Offset(0, (1 - t) * 12),
child: Transform.scale(scale: 0.95 + (t * 0.05), child: child),
),
);
},
child: FilledButton.tonal(
onPressed: isBusy ? null : () => onSelected(suggestion),
child: Text(suggestion),
), ),
); );
} }
@@ -1937,6 +1940,6 @@ Future<void> _launchUri(String url) async {
try { try {
await launchUrlString(url, mode: LaunchMode.externalApplication); await launchUrlString(url, mode: LaunchMode.externalApplication);
} catch (err) { } catch (err) {
debugPrint('Unable to open url $url: $err'); DebugLogger.log('Unable to open url $url: $err', scope: 'chat/assistant');
} }
} }

View File

@@ -1,7 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/material.dart' hide debugPrint; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
@@ -14,11 +14,6 @@ import '../../../core/providers/app_providers.dart';
import '../../auth/providers/unified_auth_providers.dart'; import '../../auth/providers/unified_auth_providers.dart';
import '../../../core/utils/debug_logger.dart'; import '../../../core/utils/debug_logger.dart';
void debugPrint(String? message, {int? wrapWidth}) {
if (message == null) return;
DebugLogger.fromLegacy(message, scope: 'chat/image-attachment');
}
// Simple global cache to prevent reloading // Simple global cache to prevent reloading
final _globalImageCache = <String, String>{}; final _globalImageCache = <String, String>{};
final _globalLoadingStates = <String, bool>{}; final _globalLoadingStates = <String, bool>{};
@@ -696,7 +691,10 @@ class FullScreenImageViewer extends ConsumerWidget {
await SharePlus.instance.share(ShareParams(files: [XFile(file.path)])); await SharePlus.instance.share(ShareParams(files: [XFile(file.path)]));
} catch (e) { } catch (e) {
// Swallowing UI feedback per requirements; keep a log for debugging // Swallowing UI feedback per requirements; keep a log for debugging
debugPrint('Failed to share image: $e'); DebugLogger.log(
'Failed to share image: $e',
scope: 'chat/image-attachment',
);
} }
} }
} }

View File

@@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer' as developer; import 'dart:developer' as developer;
import 'package:flutter/material.dart' hide debugPrint; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'core/widgets/error_boundary.dart'; import 'core/widgets/error_boundary.dart';
@@ -22,11 +22,6 @@ import 'core/providers/app_startup_providers.dart';
developer.TimelineTask? _startupTimeline; developer.TimelineTask? _startupTimeline;
void debugPrint(String? message, {int? wrapWidth}) {
if (message == null) return;
DebugLogger.fromLegacy(message, scope: 'app/main');
}
void main() { void main() {
runZonedGuarded( runZonedGuarded(
() async { () async {
@@ -41,7 +36,7 @@ void main() {
); );
final stack = details.stack; final stack = details.stack;
if (stack != null) { if (stack != null) {
debugPrint(stack.toString()); debugPrintStack(stackTrace: stack);
} }
}; };
WidgetsBinding.instance.platformDispatcher.onError = (error, stack) { WidgetsBinding.instance.platformDispatcher.onError = (error, stack) {
@@ -51,7 +46,7 @@ void main() {
error: error, error: error,
stackTrace: stack, stackTrace: stack,
); );
debugPrint(stack.toString()); debugPrintStack(stackTrace: stack);
return true; return true;
}; };
@@ -108,7 +103,7 @@ void main() {
error: error, error: error,
stackTrace: stack, stackTrace: stack,
); );
debugPrint(stack.toString()); debugPrintStack(stackTrace: stack);
}, },
); );
} }

View File

@@ -9,11 +9,6 @@ import 'outbound_task.dart';
import 'task_worker.dart'; import 'task_worker.dart';
import '../../../core/utils/debug_logger.dart'; import '../../../core/utils/debug_logger.dart';
void debugPrint(String? message, {int? wrapWidth}) {
if (message == null) return;
DebugLogger.fromLegacy(message, scope: 'tasks/queue');
}
final taskQueueProvider = final taskQueueProvider =
NotifierProvider<TaskQueueNotifier, List<OutboundTask>>( NotifierProvider<TaskQueueNotifier, List<OutboundTask>>(
TaskQueueNotifier.new, TaskQueueNotifier.new,
@@ -61,7 +56,7 @@ class TaskQueueNotifier extends Notifier<List<OutboundTask>> {
// Kick processing after load // Kick processing after load
_process(); _process();
} catch (e) { } catch (e) {
debugPrint('DEBUG: Failed to load task queue: $e'); DebugLogger.log('Failed to load task queue: $e', scope: 'tasks/queue');
} }
} }
@@ -83,7 +78,7 @@ class TaskQueueNotifier extends Notifier<List<OutboundTask>> {
final raw = retained.map((t) => t.toJson()).toList(growable: false); final raw = retained.map((t) => t.toJson()).toList(growable: false);
await prefs.setString(_prefsKey, jsonEncode(raw)); await prefs.setString(_prefsKey, jsonEncode(raw));
} catch (e) { } catch (e) {
debugPrint('DEBUG: Failed to persist task queue: $e'); DebugLogger.log('Failed to persist task queue: $e', scope: 'tasks/queue');
} }
} }
@@ -262,7 +257,10 @@ class TaskQueueNotifier extends Notifier<List<OutboundTask>> {
t, t,
]; ];
} catch (e, st) { } catch (e, st) {
debugPrint('Task failed (${task.runtimeType}): $e\n$st'); DebugLogger.log(
'Task failed (${task.runtimeType}): $e\n$st',
scope: 'tasks/queue',
);
state = [ state = [
for (final t in state) for (final t in state)
if (t.id == task.id) if (t.id == task.id)