feat(chat): Improve error handling and message versioning
This commit is contained in:
@@ -31,7 +31,8 @@ sealed class ChatMessage with _$ChatMessage {
|
|||||||
List<ChatSourceReference> sources,
|
List<ChatSourceReference> sources,
|
||||||
Map<String, dynamic>? usage,
|
Map<String, dynamic>? usage,
|
||||||
// Previous generated versions of this assistant message (OpenWebUI-style)
|
// Previous generated versions of this assistant message (OpenWebUI-style)
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
// Parsed from sibling messages in OpenWebUI history
|
||||||
|
@JsonKey(fromJson: _versionsFromJson, toJson: _versionsToJson)
|
||||||
@Default(<ChatMessageVersion>[])
|
@Default(<ChatMessageVersion>[])
|
||||||
List<ChatMessageVersion> versions,
|
List<ChatMessageVersion> versions,
|
||||||
// Error information from OpenWebUI (stored separately from content)
|
// Error information from OpenWebUI (stored separately from content)
|
||||||
@@ -134,6 +135,9 @@ abstract class ChatMessageVersion with _$ChatMessageVersion {
|
|||||||
@Default(<String>[]) List<String> followUps,
|
@Default(<String>[]) List<String> followUps,
|
||||||
@Default(<ChatCodeExecution>[]) List<ChatCodeExecution> codeExecutions,
|
@Default(<ChatCodeExecution>[]) List<ChatCodeExecution> codeExecutions,
|
||||||
Map<String, dynamic>? usage,
|
Map<String, dynamic>? usage,
|
||||||
|
// Error information preserved from the original message
|
||||||
|
@JsonKey(fromJson: _chatMessageErrorFromJson, toJson: _chatMessageErrorToJson)
|
||||||
|
ChatMessageError? error,
|
||||||
}) = _ChatMessageVersion;
|
}) = _ChatMessageVersion;
|
||||||
|
|
||||||
factory ChatMessageVersion.fromJson(Map<String, dynamic> json) =>
|
factory ChatMessageVersion.fromJson(Map<String, dynamic> json) =>
|
||||||
@@ -281,6 +285,35 @@ int? _safeInt(dynamic value) {
|
|||||||
List<String> _stringListToJson(List<String> value) =>
|
List<String> _stringListToJson(List<String> value) =>
|
||||||
List<String>.from(value, growable: false);
|
List<String>.from(value, growable: false);
|
||||||
|
|
||||||
|
/// Parse ChatMessageVersion list from JSON.
|
||||||
|
List<ChatMessageVersion> _versionsFromJson(dynamic value) {
|
||||||
|
if (value is List) {
|
||||||
|
return value
|
||||||
|
.whereType<Map>()
|
||||||
|
.map((item) {
|
||||||
|
try {
|
||||||
|
final Map<String, dynamic> versionMap = {};
|
||||||
|
item.forEach((key, v) {
|
||||||
|
versionMap[key.toString()] = v;
|
||||||
|
});
|
||||||
|
return ChatMessageVersion.fromJson(versionMap);
|
||||||
|
} catch (e) {
|
||||||
|
// Skip invalid entries
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.where((item) => item != null)
|
||||||
|
.cast<ChatMessageVersion>()
|
||||||
|
.toList(growable: false);
|
||||||
|
}
|
||||||
|
return const [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert ChatMessageVersion list to JSON.
|
||||||
|
List<Map<String, dynamic>> _versionsToJson(List<ChatMessageVersion> versions) {
|
||||||
|
return versions.map((v) => v.toJson()).toList(growable: false);
|
||||||
|
}
|
||||||
|
|
||||||
List<ChatStatusItem> _statusItemsFromJson(dynamic value) {
|
List<ChatStatusItem> _statusItemsFromJson(dynamic value) {
|
||||||
if (value is List) {
|
if (value is List) {
|
||||||
return value
|
return value
|
||||||
|
|||||||
@@ -1048,7 +1048,7 @@ class ApiService {
|
|||||||
'modelIdx': 0,
|
'modelIdx': 0,
|
||||||
'done': true,
|
'done': true,
|
||||||
if (ver.files != null) 'files': _sanitizeFilesForWebUI(ver.files),
|
if (ver.files != null) 'files': _sanitizeFilesForWebUI(ver.files),
|
||||||
// Mirror follow-ups, code executions, and sources for versions
|
// Mirror follow-ups, code executions, sources, and errors for versions
|
||||||
if (ver.followUps.isNotEmpty)
|
if (ver.followUps.isNotEmpty)
|
||||||
'followUps': List<String>.from(ver.followUps),
|
'followUps': List<String>.from(ver.followUps),
|
||||||
if (ver.codeExecutions.isNotEmpty)
|
if (ver.codeExecutions.isNotEmpty)
|
||||||
@@ -1057,6 +1057,8 @@ class ApiService {
|
|||||||
.toList(),
|
.toList(),
|
||||||
if (ver.sources.isNotEmpty)
|
if (ver.sources.isNotEmpty)
|
||||||
'sources': ver.sources.map((s) => s.toJson()).toList(),
|
'sources': ver.sources.map((s) => s.toJson()).toList(),
|
||||||
|
// Preserve error field for OpenWebUI compatibility
|
||||||
|
if (ver.error != null) 'error': ver.error!.toJson(),
|
||||||
};
|
};
|
||||||
// Link into parent (parentForVersions is always non-null here)
|
// Link into parent (parentForVersions is always non-null here)
|
||||||
if (messagesMap.containsKey(parentForVersions)) {
|
if (messagesMap.containsKey(parentForVersions)) {
|
||||||
|
|||||||
@@ -146,16 +146,24 @@ Map<String, dynamic> parseFullConversation(Map<String, dynamic> chatData) {
|
|||||||
merged['content'] = synthesized;
|
merged['content'] = synthesized;
|
||||||
}
|
}
|
||||||
|
|
||||||
messages.add(
|
final parsed = _parseOpenWebUIMessageToJson(
|
||||||
_parseOpenWebUIMessageToJson(merged, historyMsg: historyMsg),
|
merged,
|
||||||
|
historyMsg: historyMsg,
|
||||||
);
|
);
|
||||||
|
// Add versions from siblings
|
||||||
|
_addVersionsFromSiblings(parsed, msgData, historyMessagesMap);
|
||||||
|
messages.add(parsed);
|
||||||
index = j;
|
index = j;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
messages.add(
|
final parsed = _parseOpenWebUIMessageToJson(
|
||||||
_parseOpenWebUIMessageToJson(msgData, historyMsg: historyMsg),
|
msgData,
|
||||||
|
historyMsg: historyMsg,
|
||||||
);
|
);
|
||||||
|
// Add versions from siblings
|
||||||
|
_addVersionsFromSiblings(parsed, msgData, historyMessagesMap);
|
||||||
|
messages.add(parsed);
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,6 +199,120 @@ List<Map<String, dynamic>>? _extractToolCalls(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add versions from sibling messages (alternative responses with same parent).
|
||||||
|
/// Siblings are stored in `_siblings` by `_buildMessagesListFromHistory`.
|
||||||
|
void _addVersionsFromSiblings(
|
||||||
|
Map<String, dynamic> parsed,
|
||||||
|
Map<String, dynamic> msgData,
|
||||||
|
Map<String, dynamic>? historyMessagesMap,
|
||||||
|
) {
|
||||||
|
final siblings = msgData['_siblings'];
|
||||||
|
if (siblings is! List || siblings.isEmpty) return;
|
||||||
|
|
||||||
|
final versions = <Map<String, dynamic>>[];
|
||||||
|
for (final siblingData in siblings) {
|
||||||
|
if (siblingData is! Map<String, dynamic>) continue;
|
||||||
|
|
||||||
|
final siblingId = siblingData['id']?.toString();
|
||||||
|
final historyMsg = historyMessagesMap != null && siblingId != null
|
||||||
|
? (historyMessagesMap[siblingId] as Map<String, dynamic>?)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Parse the sibling as a version
|
||||||
|
final version = _parseSiblingAsVersion(siblingData, historyMsg: historyMsg);
|
||||||
|
if (version != null) {
|
||||||
|
versions.add(version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (versions.isNotEmpty) {
|
||||||
|
parsed['versions'] = versions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a sibling message as a ChatMessageVersion JSON map.
|
||||||
|
Map<String, dynamic>? _parseSiblingAsVersion(
|
||||||
|
Map<String, dynamic> msgData, {
|
||||||
|
Map<String, dynamic>? historyMsg,
|
||||||
|
}) {
|
||||||
|
// Extract content (same logic as _parseOpenWebUIMessageToJson)
|
||||||
|
dynamic content = msgData['content'];
|
||||||
|
if ((content == null || (content is String && content.isEmpty)) &&
|
||||||
|
historyMsg != null &&
|
||||||
|
historyMsg['content'] != null) {
|
||||||
|
content = historyMsg['content'];
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentString = '';
|
||||||
|
if (content is List) {
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
for (final entry in content) {
|
||||||
|
if (entry is Map && entry['type'] == 'text') {
|
||||||
|
final text = entry['text']?.toString();
|
||||||
|
if (text != null && text.isNotEmpty) {
|
||||||
|
buffer.write(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contentString = buffer.toString();
|
||||||
|
} else {
|
||||||
|
contentString = content?.toString() ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (historyMsg != null) {
|
||||||
|
final histContent = historyMsg['content'];
|
||||||
|
if (histContent is String && histContent.length > contentString.length) {
|
||||||
|
contentString = histContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract files
|
||||||
|
final effectiveFiles = msgData['files'] ?? historyMsg?['files'];
|
||||||
|
List<Map<String, dynamic>>? files;
|
||||||
|
if (effectiveFiles is List) {
|
||||||
|
final allFiles = <Map<String, dynamic>>[];
|
||||||
|
for (final entry in effectiveFiles) {
|
||||||
|
if (entry is! Map) continue;
|
||||||
|
if (entry['type'] != null && entry['url'] != null) {
|
||||||
|
final fileMap = <String, dynamic>{
|
||||||
|
'type': entry['type'],
|
||||||
|
'url': entry['url'],
|
||||||
|
};
|
||||||
|
if (entry['name'] != null) fileMap['name'] = entry['name'];
|
||||||
|
if (entry['size'] != null) fileMap['size'] = entry['size'];
|
||||||
|
allFiles.add(fileMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
files = allFiles.isNotEmpty ? allFiles : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract other fields
|
||||||
|
final sourcesRaw = historyMsg != null
|
||||||
|
? historyMsg['sources'] ?? historyMsg['citations']
|
||||||
|
: msgData['sources'] ?? msgData['citations'];
|
||||||
|
final followUpsRaw = historyMsg != null
|
||||||
|
? historyMsg['followUps'] ?? historyMsg['follow_ups']
|
||||||
|
: msgData['followUps'] ?? msgData['follow_ups'];
|
||||||
|
final codeExecRaw = historyMsg != null
|
||||||
|
? historyMsg['codeExecutions'] ?? historyMsg['code_executions']
|
||||||
|
: msgData['codeExecutions'] ?? msgData['code_executions'];
|
||||||
|
final rawUsage = _coerceJsonMap(historyMsg?['usage'] ?? msgData['usage']);
|
||||||
|
final errorData = _extractErrorData(msgData, historyMsg);
|
||||||
|
|
||||||
|
return <String, dynamic>{
|
||||||
|
'id': (msgData['id'] ?? _uuid.v4()).toString(),
|
||||||
|
'content': contentString,
|
||||||
|
'timestamp': _parseTimestamp(msgData['timestamp']).toIso8601String(),
|
||||||
|
if (msgData['model'] != null) 'model': msgData['model'].toString(),
|
||||||
|
if (files != null) 'files': files,
|
||||||
|
'sources': _parseSourcesField(sourcesRaw),
|
||||||
|
'followUps': _coerceStringList(followUpsRaw),
|
||||||
|
'codeExecutions': _parseCodeExecutionsField(codeExecRaw),
|
||||||
|
if (rawUsage.isNotEmpty) 'usage': rawUsage,
|
||||||
|
if (errorData != null) 'error': errorData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// Extract error data from OpenWebUI message format.
|
/// Extract error data from OpenWebUI message format.
|
||||||
/// OpenWebUI stores errors in a separate 'error' field with 'content' inside.
|
/// OpenWebUI stores errors in a separate 'error' field with 'content' inside.
|
||||||
/// Returns a map suitable for ChatMessageError.fromJson().
|
/// Returns a map suitable for ChatMessageError.fromJson().
|
||||||
@@ -393,6 +515,8 @@ String _resolveRole(Map<String, dynamic> msgData) {
|
|||||||
return 'user';
|
return 'user';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build the message chain from history, following parent links from currentId.
|
||||||
|
/// Also collects sibling messages (alternative versions) for each message.
|
||||||
List<Map<String, dynamic>> _buildMessagesListFromHistory(
|
List<Map<String, dynamic>> _buildMessagesListFromHistory(
|
||||||
Map<String, dynamic> history,
|
Map<String, dynamic> history,
|
||||||
) {
|
) {
|
||||||
@@ -402,6 +526,7 @@ List<Map<String, dynamic>> _buildMessagesListFromHistory(
|
|||||||
return const [];
|
return const [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build the main chain from currentId back to root
|
||||||
List<Map<String, dynamic>> buildChain(String? id) {
|
List<Map<String, dynamic>> buildChain(String? id) {
|
||||||
if (id == null) return const [];
|
if (id == null) return const [];
|
||||||
final raw = messagesMap[id];
|
final raw = messagesMap[id];
|
||||||
@@ -415,7 +540,48 @@ List<Map<String, dynamic>> _buildMessagesListFromHistory(
|
|||||||
return [msg];
|
return [msg];
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildChain(currentId);
|
final chain = buildChain(currentId);
|
||||||
|
|
||||||
|
// For each message in the chain, find sibling versions
|
||||||
|
// Siblings are other children of the same parent
|
||||||
|
for (final msg in chain) {
|
||||||
|
final parentId = msg['parentId']?.toString();
|
||||||
|
if (parentId == null || parentId.isEmpty) continue;
|
||||||
|
|
||||||
|
final parent = messagesMap[parentId];
|
||||||
|
if (parent is! Map) continue;
|
||||||
|
|
||||||
|
final childrenIds = parent['childrenIds'];
|
||||||
|
if (childrenIds is! List || childrenIds.length <= 1) continue;
|
||||||
|
|
||||||
|
// Collect sibling messages (same role, different id)
|
||||||
|
final msgId = msg['id']?.toString();
|
||||||
|
final msgRole = msg['role']?.toString();
|
||||||
|
final siblings = <Map<String, dynamic>>[];
|
||||||
|
|
||||||
|
for (final siblingId in childrenIds) {
|
||||||
|
final sibId = siblingId?.toString();
|
||||||
|
if (sibId == null || sibId == msgId) continue;
|
||||||
|
|
||||||
|
final siblingRaw = messagesMap[sibId];
|
||||||
|
if (siblingRaw is! Map) continue;
|
||||||
|
|
||||||
|
final sibling = _coerceJsonMap(siblingRaw);
|
||||||
|
final siblingRole = sibling['role']?.toString();
|
||||||
|
|
||||||
|
// Only include siblings with the same role (e.g., alternative assistant responses)
|
||||||
|
if (siblingRole == msgRole) {
|
||||||
|
sibling['id'] = sibId;
|
||||||
|
siblings.add(sibling);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (siblings.isNotEmpty) {
|
||||||
|
msg['_siblings'] = siblings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return chain;
|
||||||
}
|
}
|
||||||
|
|
||||||
DateTime _parseTimestamp(dynamic timestamp) {
|
DateTime _parseTimestamp(dynamic timestamp) {
|
||||||
|
|||||||
@@ -644,6 +644,7 @@ class ChatMessagesNotifier extends Notifier<List<ChatMessage>> {
|
|||||||
followUps: List<String>.from(last.followUps),
|
followUps: List<String>.from(last.followUps),
|
||||||
codeExecutions: List<ChatCodeExecution>.from(last.codeExecutions),
|
codeExecutions: List<ChatCodeExecution>.from(last.codeExecutions),
|
||||||
usage: last.usage == null ? null : Map<String, dynamic>.from(last.usage!),
|
usage: last.usage == null ? null : Map<String, dynamic>.from(last.usage!),
|
||||||
|
error: last.error, // Preserve error in version snapshot
|
||||||
);
|
);
|
||||||
|
|
||||||
final updated = last.copyWith(
|
final updated = last.copyWith(
|
||||||
@@ -655,6 +656,7 @@ class ChatMessagesNotifier extends Notifier<List<ChatMessage>> {
|
|||||||
codeExecutions: const [],
|
codeExecutions: const [],
|
||||||
sources: const [],
|
sources: const [],
|
||||||
usage: null,
|
usage: null,
|
||||||
|
error: null, // Clear error for new generation
|
||||||
versions: [...last.versions, snapshot],
|
versions: [...last.versions, snapshot],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1414,6 +1416,7 @@ Future<void> regenerateMessage(
|
|||||||
followUps: prev.followUps,
|
followUps: prev.followUps,
|
||||||
codeExecutions: prev.codeExecutions,
|
codeExecutions: prev.codeExecutions,
|
||||||
usage: prev.usage,
|
usage: prev.usage,
|
||||||
|
error: prev.error, // Preserve error in version snapshot
|
||||||
);
|
);
|
||||||
ref
|
ref
|
||||||
.read(chatMessagesProvider.notifier)
|
.read(chatMessagesProvider.notifier)
|
||||||
@@ -2483,16 +2486,16 @@ Future<void> _sendMessageInternal(
|
|||||||
final errorMessage = ChatMessage(
|
final errorMessage = ChatMessage(
|
||||||
id: const Uuid().v4(),
|
id: const Uuid().v4(),
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content:
|
content: '',
|
||||||
'''⚠️ There was an issue with the message format. This might be because:
|
|
||||||
|
|
||||||
• The image attachment couldn't be processed
|
|
||||||
• The request format is incompatible with the selected model
|
|
||||||
• The message contains unsupported content
|
|
||||||
|
|
||||||
Please try sending the message again, or try without attachments.''',
|
|
||||||
timestamp: DateTime.now(),
|
timestamp: DateTime.now(),
|
||||||
isStreaming: false,
|
isStreaming: false,
|
||||||
|
error: const ChatMessageError(
|
||||||
|
content: 'There was an issue with the message format. This might be '
|
||||||
|
'because the image attachment couldn\'t be processed, the request '
|
||||||
|
'format is incompatible with the selected model, or the message '
|
||||||
|
'contains unsupported content. 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('401') || e.toString().contains('403')) {
|
} else if (e.toString().contains('401') || e.toString().contains('403')) {
|
||||||
@@ -2502,11 +2505,14 @@ Please try sending the message again, or try without attachments.''',
|
|||||||
final errorMessage = ChatMessage(
|
final errorMessage = ChatMessage(
|
||||||
id: const Uuid().v4(),
|
id: const Uuid().v4(),
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content:
|
content: '',
|
||||||
'⚠️ Unable to connect to the AI model. The server returned an error (500).\n\n'
|
|
||||||
'This is typically a server-side issue. Please try again or contact your administrator.',
|
|
||||||
timestamp: DateTime.now(),
|
timestamp: DateTime.now(),
|
||||||
isStreaming: false,
|
isStreaming: false,
|
||||||
|
error: const ChatMessageError(
|
||||||
|
content: 'Unable to connect to the AI model. The server returned an '
|
||||||
|
'error (500). This is typically a server-side issue. Please try '
|
||||||
|
'again or contact your administrator.',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
ref.read(chatMessagesProvider.notifier).addMessage(errorMessage);
|
ref.read(chatMessagesProvider.notifier).addMessage(errorMessage);
|
||||||
} else if (e.toString().contains('404')) {
|
} else if (e.toString().contains('404')) {
|
||||||
@@ -2517,11 +2523,14 @@ Please try sending the message again, or try without attachments.''',
|
|||||||
final errorMessage = ChatMessage(
|
final errorMessage = ChatMessage(
|
||||||
id: const Uuid().v4(),
|
id: const Uuid().v4(),
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content:
|
content: '',
|
||||||
'🤖 The selected AI model doesn\'t seem to be available.\n\n'
|
|
||||||
'Please try selecting a different model or check with your administrator.',
|
|
||||||
timestamp: DateTime.now(),
|
timestamp: DateTime.now(),
|
||||||
isStreaming: false,
|
isStreaming: false,
|
||||||
|
error: const ChatMessageError(
|
||||||
|
content: 'The selected AI model doesn\'t seem to be available. '
|
||||||
|
'Please try selecting a different model or check with your '
|
||||||
|
'administrator.',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
ref.read(chatMessagesProvider.notifier).addMessage(errorMessage);
|
ref.read(chatMessagesProvider.notifier).addMessage(errorMessage);
|
||||||
} else {
|
} else {
|
||||||
@@ -2529,11 +2538,13 @@ Please try sending the message again, or try without attachments.''',
|
|||||||
final errorMessage = ChatMessage(
|
final errorMessage = ChatMessage(
|
||||||
id: const Uuid().v4(),
|
id: const Uuid().v4(),
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content:
|
content: '',
|
||||||
'❌ An unexpected error occurred while processing your request.\n\n'
|
|
||||||
'Please try again or check your connection.',
|
|
||||||
timestamp: DateTime.now(),
|
timestamp: DateTime.now(),
|
||||||
isStreaming: false,
|
isStreaming: false,
|
||||||
|
error: const ChatMessageError(
|
||||||
|
content: 'An unexpected error occurred while processing your request. '
|
||||||
|
'Please try again or check your connection.',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
ref.read(chatMessagesProvider.notifier).addMessage(errorMessage);
|
ref.read(chatMessagesProvider.notifier).addMessage(errorMessage);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -878,13 +878,10 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Display error banner if message has an error
|
// Display error banner if message or active version has an error
|
||||||
if (widget.message is ChatMessage &&
|
if (_getActiveError() != null) ...[
|
||||||
(widget.message as ChatMessage).error != null) ...[
|
|
||||||
const SizedBox(height: Spacing.sm),
|
const SizedBox(height: Spacing.sm),
|
||||||
_buildErrorBanner(
|
_buildErrorBanner(_getActiveError()!),
|
||||||
(widget.message as ChatMessage).error!,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
|
|
||||||
if (hasCodeExecutions) ...[
|
if (hasCodeExecutions) ...[
|
||||||
@@ -933,6 +930,21 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the error for the currently active message or version.
|
||||||
|
ChatMessageError? _getActiveError() {
|
||||||
|
if (widget.message is! ChatMessage) return null;
|
||||||
|
final msg = widget.message as ChatMessage;
|
||||||
|
|
||||||
|
// If viewing a version, return the version's error
|
||||||
|
if (_activeVersionIndex >= 0 &&
|
||||||
|
_activeVersionIndex < msg.versions.length) {
|
||||||
|
return msg.versions[_activeVersionIndex].error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise return the main message's error
|
||||||
|
return msg.error;
|
||||||
|
}
|
||||||
|
|
||||||
/// Build an error banner matching OpenWebUI's error display style.
|
/// Build an error banner matching OpenWebUI's error display style.
|
||||||
/// Shows error content in a red-tinted container with an info icon.
|
/// Shows error content in a red-tinted container with an info icon.
|
||||||
Widget _buildErrorBanner(ChatMessageError error) {
|
Widget _buildErrorBanner(ChatMessageError error) {
|
||||||
@@ -1303,8 +1315,9 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
|||||||
final messageId = _messageId;
|
final messageId = _messageId;
|
||||||
final hasSpeechText = _ttsPlainText.trim().isNotEmpty;
|
final hasSpeechText = _ttsPlainText.trim().isNotEmpty;
|
||||||
// Check for error using the error field (preferred) or legacy content detection
|
// Check for error using the error field (preferred) or legacy content detection
|
||||||
final hasErrorField = widget.message is ChatMessage &&
|
// Also check the active version's error if viewing a version
|
||||||
(widget.message as ChatMessage).error != null;
|
final activeError = _getActiveError();
|
||||||
|
final hasErrorField = activeError != null;
|
||||||
final isErrorMessage = hasErrorField ||
|
final isErrorMessage = hasErrorField ||
|
||||||
widget.message.content.contains('⚠️') ||
|
widget.message.content.contains('⚠️') ||
|
||||||
widget.message.content.contains('Error') ||
|
widget.message.content.contains('Error') ||
|
||||||
|
|||||||
Reference in New Issue
Block a user