Files
sure/mobile/lib/models/message.dart
Tristan the Katana 44e2029c6e Fix(mobile): Convert chat timestamps to local timezone on deserialization (#2701)
* Fix(mobile): Add toLocal() converter for chat timestamps so requests and responses match user local time

* Fix(mobile): localize chat timestamps at presentation layer, not model

Keep UTC in Chat/Message models; call toLocal() only in _formatTime and
_formatDateTime before reading hours/minutes. Serialize toJson with
toUtc().toIso8601String() to produce unambiguous UTC strings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-17 02:23:21 +02:00

86 lines
2.5 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'tool_call.dart';
class Message {
/// Known LLM special tokens that may leak into responses (strip from display).
/// Includes ASCII ChatML (<|...|>) and DeepSeek full-width variants (<...>).
static const _llmTokenPatterns = [
'<|start_of_sentence|>',
'<|im_start|>',
'<|im_end|>',
'<|endoftext|>',
'</s>',
// DeepSeek full-width pipe variants (U+FF5C )
'<\uFF5Cstart_of_sentence\uFF5C>',
'<\uFF5Cim_start\uFF5C>',
'<\uFF5Cim_end\uFF5C>',
'<\uFF5Cendoftext\uFF5C>',
];
/// Removes LLM tokens and trims trailing whitespace from assistant content.
static String sanitizeContent(String content) {
var out = content;
for (final token in _llmTokenPatterns) {
out = out.replaceAll(token, '');
}
out = out.replaceAll(RegExp(r'<\|[^|]*\|>'), '');
out = out.replaceAll(RegExp('<\u{FF5C}[^\u{FF5C}]*\u{FF5C}>'), '');
return out.trim();
}
final String id;
final String type;
final String role;
final String content;
final String? model;
final DateTime createdAt;
final DateTime updatedAt;
final List<ToolCall>? toolCalls;
Message({
required this.id,
required this.type,
required this.role,
required this.content,
this.model,
required this.createdAt,
required this.updatedAt,
this.toolCalls,
});
factory Message.fromJson(Map<String, dynamic> json) {
final rawContent = json['content'] as String;
final role = json['role'] as String;
final content = role == 'assistant' ? sanitizeContent(rawContent) : rawContent;
return Message(
id: json['id'].toString(),
type: json['type'] as String,
role: role,
content: content,
model: json['model'] as String?,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
toolCalls: json['tool_calls'] != null
? (json['tool_calls'] as List)
.map((tc) => ToolCall.fromJson(tc as Map<String, dynamic>))
.toList()
: null,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'type': type,
'role': role,
'content': content,
'model': model,
'created_at': createdAt.toUtc().toIso8601String(),
'updated_at': updatedAt.toUtc().toIso8601String(),
'tool_calls': toolCalls?.map((tc) => tc.toJson()).toList(),
};
}
bool get isUser => role == 'user';
bool get isAssistant => role == 'assistant';
}