refactor: fix lints
This commit is contained in:
@@ -209,7 +209,9 @@ final socketServiceProvider = Provider<SocketService?>((ref) {
|
|||||||
|
|
||||||
final activeServer = ref.watch(activeServerProvider);
|
final activeServer = ref.watch(activeServerProvider);
|
||||||
final token = ref.watch(authTokenProvider3);
|
final token = ref.watch(authTokenProvider3);
|
||||||
final transportMode = ref.watch(appSettingsProvider).socketTransportMode; // 'auto' or 'ws'
|
final transportMode = ref
|
||||||
|
.watch(appSettingsProvider)
|
||||||
|
.socketTransportMode; // 'auto' or 'ws'
|
||||||
|
|
||||||
return activeServer.maybeWhen(
|
return activeServer.maybeWhen(
|
||||||
data: (server) {
|
data: (server) {
|
||||||
@@ -223,7 +225,9 @@ final socketServiceProvider = Provider<SocketService?>((ref) {
|
|||||||
// ignore unawaited_futures
|
// ignore unawaited_futures
|
||||||
s.connect();
|
s.connect();
|
||||||
ref.onDispose(() {
|
ref.onDispose(() {
|
||||||
try { s.dispose(); } catch (_) {}
|
try {
|
||||||
|
s.dispose();
|
||||||
|
} catch (_) {}
|
||||||
});
|
});
|
||||||
return s;
|
return s;
|
||||||
},
|
},
|
||||||
@@ -373,7 +377,8 @@ final defaultModelAutoSelectionProvider = Provider<void>((ref) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: keep current selection or pick first available
|
// Fallback: keep current selection or pick first available
|
||||||
selected ??= ref.read(selectedModelProvider) ??
|
selected ??=
|
||||||
|
ref.read(selectedModelProvider) ??
|
||||||
(models.isNotEmpty ? models.first : null);
|
(models.isNotEmpty ? models.first : null);
|
||||||
|
|
||||||
if (selected != null) {
|
if (selected != null) {
|
||||||
@@ -481,11 +486,11 @@ final conversationsProvider = FutureProvider<List<Conversation>>((ref) async {
|
|||||||
conversationMap[conversation.id] = conversation.copyWith(
|
conversationMap[conversation.id] = conversation.copyWith(
|
||||||
folderId: folderIdToUse,
|
folderId: folderIdToUse,
|
||||||
);
|
);
|
||||||
final _idPreview = conversation.id.length > 8
|
final idPreview = conversation.id.length > 8
|
||||||
? conversation.id.substring(0, 8)
|
? conversation.id.substring(0, 8)
|
||||||
: conversation.id;
|
: conversation.id;
|
||||||
foundation.debugPrint(
|
foundation.debugPrint(
|
||||||
'DEBUG: Updated conversation $_idPreview with folderId: $folderIdToUse (explicit: ${explicitFolderId != null})',
|
'DEBUG: Updated conversation $idPreview with folderId: $folderIdToUse (explicit: ${explicitFolderId != null})',
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
conversationMap[conversation.id] = conversation;
|
conversationMap[conversation.id] = conversation;
|
||||||
@@ -547,11 +552,11 @@ final conversationsProvider = FutureProvider<List<Conversation>>((ref) async {
|
|||||||
// Use map to prevent duplicates - this will overwrite if ID already exists
|
// Use map to prevent duplicates - this will overwrite if ID already exists
|
||||||
conversationMap[toAdd.id] = toAdd;
|
conversationMap[toAdd.id] = toAdd;
|
||||||
existingIds.add(toAdd.id);
|
existingIds.add(toAdd.id);
|
||||||
final _idPreview = toAdd.id.length > 8
|
final idPreview = toAdd.id.length > 8
|
||||||
? toAdd.id.substring(0, 8)
|
? toAdd.id.substring(0, 8)
|
||||||
: toAdd.id;
|
: toAdd.id;
|
||||||
foundation.debugPrint(
|
foundation.debugPrint(
|
||||||
'DEBUG: Added missing conversation from folder fetch: $_idPreview -> folder ${folder.id}',
|
'DEBUG: Added missing conversation from folder fetch: $idPreview -> folder ${folder.id}',
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Create a minimal placeholder if not returned by folder API
|
// Create a minimal placeholder if not returned by folder API
|
||||||
@@ -566,11 +571,11 @@ final conversationsProvider = FutureProvider<List<Conversation>>((ref) async {
|
|||||||
// Use map to prevent duplicates
|
// Use map to prevent duplicates
|
||||||
conversationMap[convId] = placeholder;
|
conversationMap[convId] = placeholder;
|
||||||
existingIds.add(convId);
|
existingIds.add(convId);
|
||||||
final _idPreview = convId.length > 8
|
final idPreview = convId.length > 8
|
||||||
? convId.substring(0, 8)
|
? convId.substring(0, 8)
|
||||||
: convId;
|
: convId;
|
||||||
foundation.debugPrint(
|
foundation.debugPrint(
|
||||||
'DEBUG: Added placeholder conversation for missing ID: $_idPreview -> folder ${folder.id}',
|
'DEBUG: Added placeholder conversation for missing ID: $idPreview -> folder ${folder.id}',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -694,16 +699,18 @@ final defaultModelProvider = FutureProvider<Model?>((ref) async {
|
|||||||
if (userDefaultModelId != null && userDefaultModelId.isNotEmpty) {
|
if (userDefaultModelId != null && userDefaultModelId.isNotEmpty) {
|
||||||
try {
|
try {
|
||||||
// Exact ID match only
|
// Exact ID match only
|
||||||
selectedModel =
|
selectedModel = models.firstWhere(
|
||||||
models.firstWhere((model) => model.id == userDefaultModelId);
|
(model) => model.id == userDefaultModelId,
|
||||||
|
);
|
||||||
foundation.debugPrint(
|
foundation.debugPrint(
|
||||||
'DEBUG: Found user default model by ID: ${selectedModel.name}',
|
'DEBUG: Found user default model by ID: ${selectedModel.name}',
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Attempt a one-time migration if the stored value was a model name
|
// Attempt a one-time migration if the stored value was a model name
|
||||||
// from older versions. Only migrate on exact, unique name match.
|
// from older versions. Only migrate on exact, unique name match.
|
||||||
final nameMatches =
|
final nameMatches = models
|
||||||
models.where((m) => m.name == userDefaultModelId).toList();
|
.where((m) => m.name == userDefaultModelId)
|
||||||
|
.toList();
|
||||||
if (nameMatches.length == 1) {
|
if (nameMatches.length == 1) {
|
||||||
selectedModel = nameMatches.first;
|
selectedModel = nameMatches.first;
|
||||||
foundation.debugPrint(
|
foundation.debugPrint(
|
||||||
@@ -719,7 +726,8 @@ final defaultModelProvider = FutureProvider<Model?>((ref) async {
|
|||||||
'DEBUG: User default model "$userDefaultModelId" not found by ID and '
|
'DEBUG: User default model "$userDefaultModelId" not found by ID and '
|
||||||
'no unique name match. Ignoring.',
|
'no unique name match. Ignoring.',
|
||||||
);
|
);
|
||||||
selectedModel = null; // Will fall back to server default or first model
|
selectedModel =
|
||||||
|
null; // Will fall back to server default or first model
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -732,14 +740,17 @@ final defaultModelProvider = FutureProvider<Model?>((ref) async {
|
|||||||
if (defaultModelId != null && defaultModelId.isNotEmpty) {
|
if (defaultModelId != null && defaultModelId.isNotEmpty) {
|
||||||
// Find the model that matches the default model ID (ID only)
|
// Find the model that matches the default model ID (ID only)
|
||||||
try {
|
try {
|
||||||
selectedModel =
|
selectedModel = models.firstWhere(
|
||||||
models.firstWhere((model) => model.id == defaultModelId);
|
(model) => model.id == defaultModelId,
|
||||||
|
);
|
||||||
foundation.debugPrint(
|
foundation.debugPrint(
|
||||||
'DEBUG: Found server default model by ID: ${selectedModel.name}',
|
'DEBUG: Found server default model by ID: ${selectedModel.name}',
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// If server returned a name instead of ID, attempt exact name match.
|
// If server returned a name instead of ID, attempt exact name match.
|
||||||
final byName = models.where((m) => m.name == defaultModelId).toList();
|
final byName = models
|
||||||
|
.where((m) => m.name == defaultModelId)
|
||||||
|
.toList();
|
||||||
if (byName.length == 1) {
|
if (byName.length == 1) {
|
||||||
selectedModel = byName.first;
|
selectedModel = byName.first;
|
||||||
foundation.debugPrint(
|
foundation.debugPrint(
|
||||||
|
|||||||
@@ -392,11 +392,11 @@ class ApiService {
|
|||||||
debugPrint(
|
debugPrint(
|
||||||
'🔍 DEBUG: Sample chat data fields: ${chatData.keys.toList()}',
|
'🔍 DEBUG: Sample chat data fields: ${chatData.keys.toList()}',
|
||||||
);
|
);
|
||||||
final _sampleStr = chatData.toString();
|
final samplePreviewSource = chatData.toString();
|
||||||
final _preview = _sampleStr.length > 200
|
final preview = samplePreviewSource.length > 200
|
||||||
? _sampleStr.substring(0, 200)
|
? samplePreviewSource.substring(0, 200)
|
||||||
: _sampleStr;
|
: samplePreviewSource;
|
||||||
debugPrint('🔍 DEBUG: Sample chat data: $_preview...');
|
debugPrint('🔍 DEBUG: Sample chat data: $preview...');
|
||||||
}
|
}
|
||||||
|
|
||||||
final conversation = _parseOpenWebUIChat(chatData);
|
final conversation = _parseOpenWebUIChat(chatData);
|
||||||
@@ -475,8 +475,8 @@ class ApiService {
|
|||||||
|
|
||||||
// Debug logging for folder assignment
|
// Debug logging for folder assignment
|
||||||
if (folderId != null) {
|
if (folderId != null) {
|
||||||
final _idPreview = id.length > 8 ? id.substring(0, 8) : id;
|
final idPreview = id.length > 8 ? id.substring(0, 8) : id;
|
||||||
debugPrint('🔍 DEBUG: Conversation $_idPreview has folderId: $folderId');
|
debugPrint('🔍 DEBUG: Conversation $idPreview has folderId: $folderId');
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint(
|
debugPrint(
|
||||||
@@ -3357,11 +3357,11 @@ class ApiService {
|
|||||||
} else if (response.data is Map) {
|
} else if (response.data is Map) {
|
||||||
DebugLogger.log(' Object keys: ${(response.data as Map).keys}');
|
DebugLogger.log(' Object keys: ${(response.data as Map).keys}');
|
||||||
}
|
}
|
||||||
final _dataStr = response.data.toString();
|
final dataSampleSource = response.data.toString();
|
||||||
final _dataPreview = _dataStr.length > 200
|
final dataPreview = dataSampleSource.length > 200
|
||||||
? _dataStr.substring(0, 200)
|
? dataSampleSource.substring(0, 200)
|
||||||
: _dataStr;
|
: dataSampleSource;
|
||||||
DebugLogger.log(' Sample data: $_dataPreview...');
|
DebugLogger.log(' Sample data: $dataPreview...');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ $endpoint - Error: $e');
|
debugPrint('❌ $endpoint - Error: $e');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
|||||||
// Message update callbacks
|
// Message update callbacks
|
||||||
required void Function(String) appendToLastMessage,
|
required void Function(String) appendToLastMessage,
|
||||||
required void Function(String) replaceLastMessageContent,
|
required void Function(String) replaceLastMessageContent,
|
||||||
required void Function(ChatMessage Function(ChatMessage)) updateLastMessageWith,
|
required void Function(ChatMessage Function(ChatMessage))
|
||||||
|
updateLastMessageWith,
|
||||||
required void Function() finishStreaming,
|
required void Function() finishStreaming,
|
||||||
required List<ChatMessage> Function() getMessages,
|
required List<ChatMessage> Function() getMessages,
|
||||||
}) {
|
}) {
|
||||||
@@ -71,7 +72,7 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
|||||||
bool suppressSocketContent = suppressSocketContentInitially;
|
bool suppressSocketContent = suppressSocketContentInitially;
|
||||||
bool usingDynamicChannel = usingDynamicChannelInitially;
|
bool usingDynamicChannel = usingDynamicChannelInitially;
|
||||||
|
|
||||||
void _updateImagesFromCurrentContent() {
|
void updateImagesFromCurrentContent() {
|
||||||
try {
|
try {
|
||||||
final msgs = getMessages();
|
final msgs = getMessages();
|
||||||
if (msgs.isEmpty || msgs.last.role != 'assistant') return;
|
if (msgs.isEmpty || msgs.last.role != 'assistant') return;
|
||||||
@@ -236,13 +237,13 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
|||||||
for (final call in tc) {
|
for (final call in tc) {
|
||||||
if (call is Map<String, dynamic>) {
|
if (call is Map<String, dynamic>) {
|
||||||
final fn = call['function'];
|
final fn = call['function'];
|
||||||
final name =
|
final name = (fn is Map && fn['name'] is String)
|
||||||
(fn is Map && fn['name'] is String)
|
? fn['name'] as String
|
||||||
? fn['name'] as String
|
: null;
|
||||||
: null;
|
|
||||||
if (name is String && name.isNotEmpty) {
|
if (name is String && name.isNotEmpty) {
|
||||||
final msgs = getMessages();
|
final msgs = getMessages();
|
||||||
final exists = (msgs.isNotEmpty) &&
|
final exists =
|
||||||
|
(msgs.isNotEmpty) &&
|
||||||
RegExp(
|
RegExp(
|
||||||
r'<details\s+type=\"tool_calls\"[^>]*\bname=\"' +
|
r'<details\s+type=\"tool_calls\"[^>]*\bname=\"' +
|
||||||
RegExp.escape(name) +
|
RegExp.escape(name) +
|
||||||
@@ -262,20 +263,20 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
|||||||
final content = delta['content']?.toString() ?? '';
|
final content = delta['content']?.toString() ?? '';
|
||||||
if (content.isNotEmpty) {
|
if (content.isNotEmpty) {
|
||||||
appendToLastMessage(content);
|
appendToLastMessage(content);
|
||||||
_updateImagesFromCurrentContent();
|
updateImagesFromCurrentContent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
if (s.isNotEmpty) {
|
if (s.isNotEmpty) {
|
||||||
appendToLastMessage(s);
|
appendToLastMessage(s);
|
||||||
_updateImagesFromCurrentContent();
|
updateImagesFromCurrentContent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (s.isNotEmpty) {
|
if (s.isNotEmpty) {
|
||||||
appendToLastMessage(s);
|
appendToLastMessage(s);
|
||||||
_updateImagesFromCurrentContent();
|
updateImagesFromCurrentContent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (line is Map) {
|
} else if (line is Map) {
|
||||||
@@ -320,10 +321,12 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
|||||||
: null;
|
: null;
|
||||||
if (name is String && name.isNotEmpty) {
|
if (name is String && name.isNotEmpty) {
|
||||||
final msgs = getMessages();
|
final msgs = getMessages();
|
||||||
final exists = (msgs.isNotEmpty) &&
|
final exists =
|
||||||
|
(msgs.isNotEmpty) &&
|
||||||
RegExp(
|
RegExp(
|
||||||
r'<details\s+type=\"tool_calls\"[^>]*\bname=\"' +
|
r'<details\s+type=\"tool_calls\"[^>]*\bname=\"' +
|
||||||
RegExp.escape(name) + r'\"',
|
RegExp.escape(name) +
|
||||||
|
r'\"',
|
||||||
multiLine: true,
|
multiLine: true,
|
||||||
).hasMatch(msgs.last.content);
|
).hasMatch(msgs.last.content);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
@@ -353,10 +356,12 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
|||||||
: null;
|
: null;
|
||||||
if (name is String && name.isNotEmpty) {
|
if (name is String && name.isNotEmpty) {
|
||||||
final msgs = getMessages();
|
final msgs = getMessages();
|
||||||
final exists = (msgs.isNotEmpty) &&
|
final exists =
|
||||||
|
(msgs.isNotEmpty) &&
|
||||||
RegExp(
|
RegExp(
|
||||||
r'<details\s+type=\"tool_calls\"[^>]*\bname=\"' +
|
r'<details\s+type=\"tool_calls\"[^>]*\bname=\"' +
|
||||||
RegExp.escape(name) + r'\"',
|
RegExp.escape(name) +
|
||||||
|
r'\"',
|
||||||
multiLine: true,
|
multiLine: true,
|
||||||
).hasMatch(msgs.last.content);
|
).hasMatch(msgs.last.content);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
@@ -372,7 +377,7 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
|||||||
final content = delta['content']?.toString() ?? '';
|
final content = delta['content']?.toString() ?? '';
|
||||||
if (content.isNotEmpty) {
|
if (content.isNotEmpty) {
|
||||||
appendToLastMessage(content);
|
appendToLastMessage(content);
|
||||||
_updateImagesFromCurrentContent();
|
updateImagesFromCurrentContent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -409,7 +414,9 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
|||||||
final list = chatObj['messages'];
|
final list = chatObj['messages'];
|
||||||
if (list is List) {
|
if (list is List) {
|
||||||
final target = list.firstWhere(
|
final target = list.firstWhere(
|
||||||
(m) => (m is Map && (m['id']?.toString() == assistantMessageId)),
|
(m) =>
|
||||||
|
(m is Map &&
|
||||||
|
(m['id']?.toString() == assistantMessageId)),
|
||||||
orElse: () => null,
|
orElse: () => null,
|
||||||
);
|
);
|
||||||
if (target != null) {
|
if (target != null) {
|
||||||
@@ -431,7 +438,8 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
|||||||
final history = chatObj['history'];
|
final history = chatObj['history'];
|
||||||
if (history is Map && history['messages'] is Map) {
|
if (history is Map && history['messages'] is Map) {
|
||||||
final Map<String, dynamic> messagesMap =
|
final Map<String, dynamic> messagesMap =
|
||||||
(history['messages'] as Map).cast<String, dynamic>();
|
(history['messages'] as Map)
|
||||||
|
.cast<String, dynamic>();
|
||||||
final msg = messagesMap[assistantMessageId];
|
final msg = messagesMap[assistantMessageId];
|
||||||
if (msg is Map) {
|
if (msg is Map) {
|
||||||
final rawContent = msg['content'];
|
final rawContent = msg['content'];
|
||||||
@@ -454,7 +462,8 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
|||||||
replaceLastMessageContent(content);
|
replaceLastMessageContent(content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (_) {} finally {
|
} catch (_) {
|
||||||
|
} finally {
|
||||||
finishStreaming();
|
finishStreaming();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -483,21 +492,23 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
|||||||
}
|
}
|
||||||
if (content.isNotEmpty) {
|
if (content.isNotEmpty) {
|
||||||
// Replace current assistant message with a readable error
|
// Replace current assistant message with a readable error
|
||||||
replaceLastMessageContent('⚠️ ' + content);
|
replaceLastMessageContent('⚠️ $content');
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
// Ensure UI exits streaming state
|
// Ensure UI exits streaming state
|
||||||
finishStreaming();
|
finishStreaming();
|
||||||
} else if ((type == 'chat:message:delta' || type == 'message') && payload != null) {
|
} else if ((type == 'chat:message:delta' || type == 'message') &&
|
||||||
|
payload != null) {
|
||||||
// Incremental message content over socket; respect suppression on SSE-driven flows
|
// Incremental message content over socket; respect suppression on SSE-driven flows
|
||||||
if (!suppressSocketContent) {
|
if (!suppressSocketContent) {
|
||||||
final content = payload['content']?.toString() ?? '';
|
final content = payload['content']?.toString() ?? '';
|
||||||
if (content.isNotEmpty) {
|
if (content.isNotEmpty) {
|
||||||
appendToLastMessage(content);
|
appendToLastMessage(content);
|
||||||
_updateImagesFromCurrentContent();
|
updateImagesFromCurrentContent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if ((type == 'chat:message' || type == 'replace') && payload != null) {
|
} else if ((type == 'chat:message' || type == 'replace') &&
|
||||||
|
payload != null) {
|
||||||
// Full message replacement over socket; respect suppression on SSE-driven flows
|
// Full message replacement over socket; respect suppression on SSE-driven flows
|
||||||
if (!suppressSocketContent) {
|
if (!suppressSocketContent) {
|
||||||
final content = payload['content']?.toString() ?? '';
|
final content = payload['content']?.toString() ?? '';
|
||||||
@@ -600,10 +611,9 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
|||||||
} else if (type == 'event:status' && payload != null) {
|
} else if (type == 'event:status' && payload != null) {
|
||||||
final status = payload['status']?.toString() ?? '';
|
final status = payload['status']?.toString() ?? '';
|
||||||
if (status.isNotEmpty) {
|
if (status.isNotEmpty) {
|
||||||
updateLastMessageWith((m) => m.copyWith(metadata: {
|
updateLastMessageWith(
|
||||||
...?m.metadata,
|
(m) => m.copyWith(metadata: {...?m.metadata, 'status': status}),
|
||||||
'status': status,
|
);
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
} else if (type == 'event:tool' && payload != null) {
|
} else if (type == 'event:tool' && payload != null) {
|
||||||
// Accept files from both 'result' and 'files'
|
// Accept files from both 'result' and 'files'
|
||||||
@@ -624,7 +634,7 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
|||||||
final content = payload['content']?.toString() ?? '';
|
final content = payload['content']?.toString() ?? '';
|
||||||
if (content.isNotEmpty) {
|
if (content.isNotEmpty) {
|
||||||
appendToLastMessage(content);
|
appendToLastMessage(content);
|
||||||
_updateImagesFromCurrentContent();
|
updateImagesFromCurrentContent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
@@ -640,7 +650,7 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
|||||||
final content = payload['content']?.toString() ?? '';
|
final content = payload['content']?.toString() ?? '';
|
||||||
if (content.isNotEmpty) {
|
if (content.isNotEmpty) {
|
||||||
appendToLastMessage(content);
|
appendToLastMessage(content);
|
||||||
_updateImagesFromCurrentContent();
|
updateImagesFromCurrentContent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
@@ -656,7 +666,9 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
try {
|
try {
|
||||||
final msgs = getMessages();
|
final msgs = getMessages();
|
||||||
if (msgs.isNotEmpty && msgs.last.role == 'assistant' && msgs.last.isStreaming) {
|
if (msgs.isNotEmpty &&
|
||||||
|
msgs.last.role == 'assistant' &&
|
||||||
|
msgs.last.isStreaming) {
|
||||||
finishStreaming();
|
finishStreaming();
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
@@ -681,17 +693,21 @@ StreamSubscription<String> attachUnifiedChunkedStreaming({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSearching && (chunk.contains('[/SEARCHING]') || chunk.contains('Search complete'))) {
|
if (isSearching &&
|
||||||
|
(chunk.contains('[/SEARCHING]') ||
|
||||||
|
chunk.contains('Search complete'))) {
|
||||||
isSearching = false;
|
isSearching = false;
|
||||||
updateLastMessageWith(
|
updateLastMessageWith(
|
||||||
(message) => message.copyWith(metadata: {'webSearchActive': false}),
|
(message) => message.copyWith(metadata: {'webSearchActive': false}),
|
||||||
);
|
);
|
||||||
effectiveChunk = effectiveChunk.replaceAll('[SEARCHING]', '').replaceAll('[/SEARCHING]', '');
|
effectiveChunk = effectiveChunk
|
||||||
|
.replaceAll('[SEARCHING]', '')
|
||||||
|
.replaceAll('[/SEARCHING]', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (effectiveChunk.trim().isNotEmpty) {
|
if (effectiveChunk.trim().isNotEmpty) {
|
||||||
appendToLastMessage(effectiveChunk);
|
appendToLastMessage(effectiveChunk);
|
||||||
_updateImagesFromCurrentContent();
|
updateImagesFromCurrentContent();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDone: () async {
|
onDone: () async {
|
||||||
|
|||||||
@@ -49,7 +49,9 @@ class ReasoningParser {
|
|||||||
if (openingIdx >= 0 && !content.contains('</details>')) {
|
if (openingIdx >= 0 && !content.contains('</details>')) {
|
||||||
final after = content.substring(openingIdx);
|
final after = content.substring(openingIdx);
|
||||||
// Try to extract optional summary
|
// Try to extract optional summary
|
||||||
final summaryMatch = RegExp(r'<summary>([^<]*)<\/summary>').firstMatch(after);
|
final summaryMatch = RegExp(
|
||||||
|
r'<summary>([^<]*)<\/summary>',
|
||||||
|
).firstMatch(after);
|
||||||
final summary = (summaryMatch?.group(1) ?? '').trim();
|
final summary = (summaryMatch?.group(1) ?? '').trim();
|
||||||
final reasoning = after
|
final reasoning = after
|
||||||
.replaceAll(RegExp(r'^<details[^>]*>'), '')
|
.replaceAll(RegExp(r'^<details[^>]*>'), '')
|
||||||
@@ -80,7 +82,11 @@ class ReasoningParser {
|
|||||||
for (final pair in tagPairs) {
|
for (final pair in tagPairs) {
|
||||||
final start = RegExp.escape(pair[0]);
|
final start = RegExp.escape(pair[0]);
|
||||||
final end = RegExp.escape(pair[1]);
|
final end = RegExp.escape(pair[1]);
|
||||||
final tagRegex = RegExp('($start)([\s\S]*?)($end)', multiLine: true, dotAll: true);
|
final tagRegex = RegExp(
|
||||||
|
'($start)(.*?)($end)',
|
||||||
|
multiLine: true,
|
||||||
|
dotAll: true,
|
||||||
|
);
|
||||||
final match = tagRegex.firstMatch(content);
|
final match = tagRegex.firstMatch(content);
|
||||||
if (match != null) {
|
if (match != null) {
|
||||||
final reasoning = (match.group(2) ?? '').trim();
|
final reasoning = (match.group(2) ?? '').trim();
|
||||||
@@ -144,7 +150,8 @@ class ReasoningParser {
|
|||||||
if (nextDetails == -1 && nextRawStart == -1) {
|
if (nextDetails == -1 && nextRawStart == -1) {
|
||||||
nextIdx = -1;
|
nextIdx = -1;
|
||||||
kind = 'none';
|
kind = 'none';
|
||||||
} else if (nextDetails != -1 && (nextRawStart == -1 || nextDetails < nextRawStart)) {
|
} else if (nextDetails != -1 &&
|
||||||
|
(nextRawStart == -1 || nextDetails < nextRawStart)) {
|
||||||
nextIdx = nextDetails;
|
nextIdx = nextDetails;
|
||||||
kind = 'details';
|
kind = 'details';
|
||||||
} else {
|
} else {
|
||||||
@@ -219,7 +226,9 @@ class ReasoningParser {
|
|||||||
if (depth != 0) {
|
if (depth != 0) {
|
||||||
// Unclosed; treat as streaming partial
|
// Unclosed; treat as streaming partial
|
||||||
final after = content.substring(openEnd + 1);
|
final after = content.substring(openEnd + 1);
|
||||||
final summaryMatch = RegExp(r'<summary>([^<]*)<\/summary>').firstMatch(after);
|
final summaryMatch = RegExp(
|
||||||
|
r'<summary>([^<]*)<\/summary>',
|
||||||
|
).firstMatch(after);
|
||||||
final summary = (summaryMatch?.group(1) ?? '').trim();
|
final summary = (summaryMatch?.group(1) ?? '').trim();
|
||||||
final reasoning = after
|
final reasoning = after
|
||||||
.replaceAll(RegExp(r'^\s*<summary>[\s\S]*?<\/summary>'), '')
|
.replaceAll(RegExp(r'^\s*<summary>[\s\S]*?<\/summary>'), '')
|
||||||
@@ -238,8 +247,13 @@ class ReasoningParser {
|
|||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
// Closed block: extract inner content
|
// Closed block: extract inner content
|
||||||
final inner = content.substring(openEnd + 1, i - 10); // without </details>
|
final inner = content.substring(
|
||||||
final sumMatch = RegExp(r'<summary>([^<]*)<\/summary>').firstMatch(inner);
|
openEnd + 1,
|
||||||
|
i - 10,
|
||||||
|
); // without </details>
|
||||||
|
final sumMatch = RegExp(
|
||||||
|
r'<summary>([^<]*)<\/summary>',
|
||||||
|
).firstMatch(inner);
|
||||||
final summary = (sumMatch?.group(1) ?? '').trim();
|
final summary = (sumMatch?.group(1) ?? '').trim();
|
||||||
final reasoning = inner
|
final reasoning = inner
|
||||||
.replaceAll(RegExp(r'<summary>[\s\S]*?<\/summary>'), '')
|
.replaceAll(RegExp(r'<summary>[\s\S]*?<\/summary>'), '')
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class ToolCallsParser {
|
|||||||
.replaceAll('&', '&')
|
.replaceAll('&', '&')
|
||||||
.replaceAll('&', '&');
|
.replaceAll('&', '&');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents a mixed stream of text and tool-call entries in original order
|
/// Represents a mixed stream of text and tool-call entries in original order
|
||||||
/// as they appeared in the content.
|
/// as they appeared in the content.
|
||||||
static List<ToolCallsSegment>? segments(String content) {
|
static List<ToolCallsSegment>? segments(String content) {
|
||||||
@@ -97,7 +98,9 @@ class ToolCallsParser {
|
|||||||
i = nextOpen + 8; // '<details'
|
i = nextOpen + 8; // '<details'
|
||||||
} else {
|
} else {
|
||||||
depth--;
|
depth--;
|
||||||
i = (nextClose != -1) ? nextClose + 10 : content.length; // '</details>'
|
i = (nextClose != -1)
|
||||||
|
? nextClose + 10
|
||||||
|
: content.length; // '</details>'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,17 +108,16 @@ class ToolCallsParser {
|
|||||||
|
|
||||||
if (isToolCalls) {
|
if (isToolCalls) {
|
||||||
// Decode attributes for tool call tile
|
// Decode attributes for tool call tile
|
||||||
dynamic _decode(String? s) {
|
dynamic decodeAttribute(String? source) {
|
||||||
if (s == null || s.isEmpty) return null;
|
if (source == null || source.isEmpty) return null;
|
||||||
try {
|
try {
|
||||||
final unescaped = _unescapeHtml(s);
|
final unescaped = _unescapeHtml(source);
|
||||||
return json.decode(unescaped);
|
return json.decode(unescaped);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// If JSON decode fails, return unescaped string for display
|
|
||||||
try {
|
try {
|
||||||
return _unescapeHtml(s);
|
return _unescapeHtml(source);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return s;
|
return source;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,9 +125,9 @@ class ToolCallsParser {
|
|||||||
final id = (attrs['id'] ?? '');
|
final id = (attrs['id'] ?? '');
|
||||||
final name = (attrs['name'] ?? 'tool');
|
final name = (attrs['name'] ?? 'tool');
|
||||||
final done = (attrs['done'] == 'true');
|
final done = (attrs['done'] == 'true');
|
||||||
final args = _decode(attrs['arguments']);
|
final args = decodeAttribute(attrs['arguments']);
|
||||||
final result = _decode(attrs['result']);
|
final result = decodeAttribute(attrs['result']);
|
||||||
final files = _decode(attrs['files']);
|
final files = decodeAttribute(attrs['files']);
|
||||||
|
|
||||||
segs.add(
|
segs.add(
|
||||||
ToolCallsSegment.entry(
|
ToolCallsSegment.entry(
|
||||||
@@ -207,7 +209,9 @@ class ToolCallsParser {
|
|||||||
if (parsed == null) return content;
|
if (parsed == null) return content;
|
||||||
final buf = StringBuffer();
|
final buf = StringBuffer();
|
||||||
for (final c in parsed.toolCalls) {
|
for (final c in parsed.toolCalls) {
|
||||||
buf.writeln(c.done ? 'Tool Executed: ${c.name}' : 'Running tool: ${c.name}…');
|
buf.writeln(
|
||||||
|
c.done ? 'Tool Executed: ${c.name}' : 'Running tool: ${c.name}…',
|
||||||
|
);
|
||||||
final args = _prettyMaybe(c.arguments, max: 400);
|
final args = _prettyMaybe(c.arguments, max: 400);
|
||||||
final res = _prettyMaybe(c.result, max: 800);
|
final res = _prettyMaybe(c.result, max: 800);
|
||||||
if (args.isNotEmpty) {
|
if (args.isNotEmpty) {
|
||||||
@@ -239,8 +243,8 @@ class ToolCallsParser {
|
|||||||
|
|
||||||
/// Sanitize assistant/user content before sending to the API, mirroring
|
/// Sanitize assistant/user content before sending to the API, mirroring
|
||||||
/// the web client's `processDetails` behavior:
|
/// the web client's `processDetails` behavior:
|
||||||
/// - Remove <details type="reasoning"> and <details type="code_interpreter"> blocks
|
/// - Remove <details type="reasoning"> and <details type="code_interpreter"> blocks
|
||||||
/// - Replace <details type="tool_calls" ...>...</details> blocks with the
|
/// - Replace <details type="tool_calls" ...>...</details> blocks with the
|
||||||
/// JSON-serialized `result` attribute (as a quoted string) when available;
|
/// JSON-serialized `result` attribute (as a quoted string) when available;
|
||||||
/// otherwise replace with an empty string.
|
/// otherwise replace with an empty string.
|
||||||
static String sanitizeForApi(String content) {
|
static String sanitizeForApi(String content) {
|
||||||
@@ -251,7 +255,7 @@ class ToolCallsParser {
|
|||||||
for (final t in removeTypes) {
|
for (final t in removeTypes) {
|
||||||
content = content.replaceAll(
|
content = content.replaceAll(
|
||||||
RegExp(
|
RegExp(
|
||||||
'<details\\s+type=\"${t}\"[^>]*>[\\s\\S]*?<\\/details>',
|
'<details\\s+type="$t"[^>]*>[\\s\\S]*?</details>',
|
||||||
multiLine: true,
|
multiLine: true,
|
||||||
dotAll: true,
|
dotAll: true,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ class ChatMessagesNotifier extends StateNotifier<List<ChatMessage>> {
|
|||||||
final msgId = last.id;
|
final msgId = last.id;
|
||||||
final chatId = activeConv?.id;
|
final chatId = activeConv?.id;
|
||||||
if (apiSvc != null && chatId != null && chatId.isNotEmpty) {
|
if (apiSvc != null && chatId != null && chatId.isNotEmpty) {
|
||||||
final resp = await apiSvc.dio.get('/api/v1/chats/' + chatId);
|
final resp = await apiSvc.dio.get('/api/v1/chats/$chatId');
|
||||||
final data = resp.data as Map<String, dynamic>;
|
final data = resp.data as Map<String, dynamic>;
|
||||||
String content = '';
|
String content = '';
|
||||||
final chatObj = data['chat'] as Map<String, dynamic>?;
|
final chatObj = data['chat'] as Map<String, dynamic>?;
|
||||||
@@ -948,11 +948,11 @@ Future<void> regenerateMessage(
|
|||||||
final bool isBackgroundWebSearchPre = webSearchEnabled;
|
final bool isBackgroundWebSearchPre = webSearchEnabled;
|
||||||
|
|
||||||
// Dispatch using unified send pipeline (background tools flow)
|
// Dispatch using unified send pipeline (background tools flow)
|
||||||
final bool _isBackgroundFlowPre =
|
final bool isBackgroundFlowPre =
|
||||||
isBackgroundToolsFlowPre ||
|
isBackgroundToolsFlowPre ||
|
||||||
isBackgroundWebSearchPre ||
|
isBackgroundWebSearchPre ||
|
||||||
imageGenerationEnabled;
|
imageGenerationEnabled;
|
||||||
final bool _passSocketSession = wantSessionBinding && _isBackgroundFlowPre;
|
final bool passSocketSession = wantSessionBinding && isBackgroundFlowPre;
|
||||||
final response = api!.sendMessage(
|
final response = api!.sendMessage(
|
||||||
messages: conversationMessages,
|
messages: conversationMessages,
|
||||||
model: selectedModel.id,
|
model: selectedModel.id,
|
||||||
@@ -961,7 +961,7 @@ Future<void> regenerateMessage(
|
|||||||
enableWebSearch: webSearchEnabled,
|
enableWebSearch: webSearchEnabled,
|
||||||
enableImageGeneration: imageGenerationEnabled,
|
enableImageGeneration: imageGenerationEnabled,
|
||||||
modelItem: modelItem,
|
modelItem: modelItem,
|
||||||
sessionIdOverride: _passSocketSession ? socketSessionId : null,
|
sessionIdOverride: passSocketSession ? socketSessionId : null,
|
||||||
toolServers: toolServers,
|
toolServers: toolServers,
|
||||||
backgroundTasks: bgTasks,
|
backgroundTasks: bgTasks,
|
||||||
responseMessageId: assistantMessageId,
|
responseMessageId: assistantMessageId,
|
||||||
@@ -971,7 +971,7 @@ Future<void> regenerateMessage(
|
|||||||
final sessionId = response.sessionId;
|
final sessionId = response.sessionId;
|
||||||
|
|
||||||
// New unified streaming path via helper; bypass old inline socket block
|
// New unified streaming path via helper; bypass old inline socket block
|
||||||
final bool _isBackgroundFlow =
|
final bool isBackgroundFlow =
|
||||||
isBackgroundToolsFlowPre ||
|
isBackgroundToolsFlowPre ||
|
||||||
isBackgroundWebSearchPre ||
|
isBackgroundWebSearchPre ||
|
||||||
imageGenerationEnabled ||
|
imageGenerationEnabled ||
|
||||||
@@ -982,7 +982,7 @@ Future<void> regenerateMessage(
|
|||||||
) {
|
) {
|
||||||
final mergedMeta = {
|
final mergedMeta = {
|
||||||
if (m.metadata != null) ...m.metadata!,
|
if (m.metadata != null) ...m.metadata!,
|
||||||
'backgroundFlow': _isBackgroundFlow,
|
'backgroundFlow': isBackgroundFlow,
|
||||||
if (isBackgroundWebSearchPre) 'webSearchFlow': true,
|
if (isBackgroundWebSearchPre) 'webSearchFlow': true,
|
||||||
if (imageGenerationEnabled) 'imageGenerationFlow': true,
|
if (imageGenerationEnabled) 'imageGenerationFlow': true,
|
||||||
};
|
};
|
||||||
@@ -990,11 +990,11 @@ Future<void> regenerateMessage(
|
|||||||
});
|
});
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
final _sendStreamSub = attachUnifiedChunkedStreaming(
|
final sendStreamSub = attachUnifiedChunkedStreaming(
|
||||||
stream: stream,
|
stream: stream,
|
||||||
webSearchEnabled: webSearchEnabled,
|
webSearchEnabled: webSearchEnabled,
|
||||||
isBackgroundFlow: _isBackgroundFlow,
|
isBackgroundFlow: isBackgroundFlow,
|
||||||
suppressSocketContentInitially: !_isBackgroundFlow,
|
suppressSocketContentInitially: !isBackgroundFlow,
|
||||||
usingDynamicChannelInitially: false,
|
usingDynamicChannelInitially: false,
|
||||||
assistantMessageId: assistantMessageId,
|
assistantMessageId: assistantMessageId,
|
||||||
modelId: selectedModel.id,
|
modelId: selectedModel.id,
|
||||||
@@ -1014,7 +1014,7 @@ Future<void> regenerateMessage(
|
|||||||
ref.read(chatMessagesProvider.notifier).finishStreaming(),
|
ref.read(chatMessagesProvider.notifier).finishStreaming(),
|
||||||
getMessages: () => ref.read(chatMessagesProvider),
|
getMessages: () => ref.read(chatMessagesProvider),
|
||||||
);
|
);
|
||||||
ref.read(chatMessagesProvider.notifier).setMessageStream(_sendStreamSub);
|
ref.read(chatMessagesProvider.notifier).setMessageStream(sendStreamSub);
|
||||||
return;
|
return;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
rethrow;
|
rethrow;
|
||||||
@@ -1482,7 +1482,7 @@ Future<void> _sendMessageInternal(
|
|||||||
|
|
||||||
if (socketService != null) {
|
if (socketService != null) {
|
||||||
// Activity-based watchdog for chat/channel events (resets on activity)
|
// Activity-based watchdog for chat/channel events (resets on activity)
|
||||||
final _chatWatchdog = InactivityWatchdog(
|
final chatWatchdog = InactivityWatchdog(
|
||||||
window: const Duration(minutes: 5),
|
window: const Duration(minutes: 5),
|
||||||
onTimeout: () {
|
onTimeout: () {
|
||||||
try {
|
try {
|
||||||
@@ -1510,7 +1510,7 @@ Future<void> _sendMessageInternal(
|
|||||||
DebugLogger.stream('Socket chat-events: type=$type');
|
DebugLogger.stream('Socket chat-events: type=$type');
|
||||||
// Any chat event indicates activity; reset inactivity watchdog
|
// Any chat event indicates activity; reset inactivity watchdog
|
||||||
// (watchdog defined below, near handler registration)
|
// (watchdog defined below, near handler registration)
|
||||||
_chatWatchdog.ping();
|
chatWatchdog.ping();
|
||||||
if (type == 'chat:completion' && payload != null) {
|
if (type == 'chat:completion' && payload != null) {
|
||||||
if (payload is Map<String, dynamic>) {
|
if (payload is Map<String, dynamic>) {
|
||||||
// Provider may emit tool_calls at the top level
|
// Provider may emit tool_calls at the top level
|
||||||
@@ -1529,9 +1529,7 @@ Future<void> _sendMessageInternal(
|
|||||||
final exists =
|
final exists =
|
||||||
(msgs.isNotEmpty) &&
|
(msgs.isNotEmpty) &&
|
||||||
RegExp(
|
RegExp(
|
||||||
r'<details\s+type=\"tool_calls\"[^>]*\bname=\"' +
|
'<details\\s+type="tool_calls"[^>]*\\bname="${RegExp.escape(name)}"',
|
||||||
RegExp.escape(name) +
|
|
||||||
r'\"',
|
|
||||||
multiLine: true,
|
multiLine: true,
|
||||||
).hasMatch(msgs.last.content);
|
).hasMatch(msgs.last.content);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
@@ -1567,9 +1565,7 @@ Future<void> _sendMessageInternal(
|
|||||||
final exists =
|
final exists =
|
||||||
(msgs.isNotEmpty) &&
|
(msgs.isNotEmpty) &&
|
||||||
RegExp(
|
RegExp(
|
||||||
r'<details\s+type=\"tool_calls\"[^>]*\bname=\"' +
|
'<details\\s+type="tool_calls"[^>]*\\bname="${RegExp.escape(name)}"',
|
||||||
RegExp.escape(name) +
|
|
||||||
r'\"',
|
|
||||||
multiLine: true,
|
multiLine: true,
|
||||||
).hasMatch(msgs.last.content);
|
).hasMatch(msgs.last.content);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
@@ -1628,8 +1624,8 @@ Future<void> _sendMessageInternal(
|
|||||||
socketService.offChatEvents();
|
socketService.offChatEvents();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
try {
|
try {
|
||||||
_chatWatchdog.ping(); // ensure timer exists
|
chatWatchdog.ping(); // ensure timer exists
|
||||||
_chatWatchdog.stop();
|
chatWatchdog.stop();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
// Notify server that chat is completed (mirrors web client)
|
// Notify server that chat is completed (mirrors web client)
|
||||||
@@ -1744,7 +1740,7 @@ Future<void> _sendMessageInternal(
|
|||||||
// Normal path: finish now
|
// Normal path: finish now
|
||||||
ref.read(chatMessagesProvider.notifier).finishStreaming();
|
ref.read(chatMessagesProvider.notifier).finishStreaming();
|
||||||
try {
|
try {
|
||||||
_chatWatchdog.stop();
|
chatWatchdog.stop();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1767,7 +1763,7 @@ Future<void> _sendMessageInternal(
|
|||||||
final s = line.trim();
|
final s = line.trim();
|
||||||
// Dynamic channel activity
|
// Dynamic channel activity
|
||||||
try {
|
try {
|
||||||
_chatWatchdog.ping();
|
chatWatchdog.ping();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
DebugLogger.stream(
|
DebugLogger.stream(
|
||||||
'Socket [$channel] line=${s.length > 160 ? '${s.substring(0, 160)}…' : s}',
|
'Socket [$channel] line=${s.length > 160 ? '${s.substring(0, 160)}…' : s}',
|
||||||
@@ -1850,9 +1846,7 @@ Future<void> _sendMessageInternal(
|
|||||||
final exists =
|
final exists =
|
||||||
(msgs.isNotEmpty) &&
|
(msgs.isNotEmpty) &&
|
||||||
RegExp(
|
RegExp(
|
||||||
r'<details\\s+type=\"tool_calls\"[^>]*\\bname=\"' +
|
'<details\\s+type="tool_calls"[^>]*\\bname="${RegExp.escape(name)}"',
|
||||||
RegExp.escape(name) +
|
|
||||||
r'\"',
|
|
||||||
multiLine: true,
|
multiLine: true,
|
||||||
).hasMatch(msgs.last.content);
|
).hasMatch(msgs.last.content);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
@@ -1944,7 +1938,7 @@ Future<void> _sendMessageInternal(
|
|||||||
if (content.isNotEmpty) {
|
if (content.isNotEmpty) {
|
||||||
ref
|
ref
|
||||||
.read(chatMessagesProvider.notifier)
|
.read(chatMessagesProvider.notifier)
|
||||||
.replaceLastMessageContent('⚠️ ' + content);
|
.replaceLastMessageContent('⚠️ $content');
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
ref.read(chatMessagesProvider.notifier).finishStreaming();
|
ref.read(chatMessagesProvider.notifier).finishStreaming();
|
||||||
@@ -2060,7 +2054,7 @@ Future<void> _sendMessageInternal(
|
|||||||
.read(chatMessagesProvider.notifier)
|
.read(chatMessagesProvider.notifier)
|
||||||
.appendToLastMessage(content);
|
.appendToLastMessage(content);
|
||||||
_updateImagesFromCurrentContent(ref);
|
_updateImagesFromCurrentContent(ref);
|
||||||
_chatWatchdog.ping();
|
chatWatchdog.ping();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
@@ -2068,7 +2062,7 @@ Future<void> _sendMessageInternal(
|
|||||||
|
|
||||||
socketService.onChannelEvents(channelEventsHandler);
|
socketService.onChannelEvents(channelEventsHandler);
|
||||||
// Start activity watchdog
|
// Start activity watchdog
|
||||||
_chatWatchdog.ping();
|
chatWatchdog.ping();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare streaming and background handling
|
// Prepare streaming and background handling
|
||||||
@@ -2123,14 +2117,14 @@ Future<void> _sendMessageInternal(
|
|||||||
|
|
||||||
// Helpers were defined above
|
// Helpers were defined above
|
||||||
|
|
||||||
int _chunkSeq = 0;
|
int chunkSeq = 0;
|
||||||
final streamSubscription = persistentController.stream.listen(
|
final streamSubscription = persistentController.stream.listen(
|
||||||
(chunk) {
|
(chunk) {
|
||||||
_chunkSeq += 1;
|
chunkSeq += 1;
|
||||||
try {
|
try {
|
||||||
persistentService.updateStreamProgress(
|
persistentService.updateStreamProgress(
|
||||||
streamId,
|
streamId,
|
||||||
chunkSequence: _chunkSeq,
|
chunkSequence: chunkSeq,
|
||||||
appendedContent: chunk,
|
appendedContent: chunk,
|
||||||
);
|
);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
@@ -3030,7 +3024,7 @@ void _attachSocketStreamingHandlers({
|
|||||||
final api = ref.read(apiServiceProvider);
|
final api = ref.read(apiServiceProvider);
|
||||||
|
|
||||||
// Activity-based watchdog for socket-driven streaming (resets on activity)
|
// Activity-based watchdog for socket-driven streaming (resets on activity)
|
||||||
final _socketWatchdog = InactivityWatchdog(
|
final socketWatchdog = InactivityWatchdog(
|
||||||
window: const Duration(minutes: 5),
|
window: const Duration(minutes: 5),
|
||||||
onTimeout: () {
|
onTimeout: () {
|
||||||
try {
|
try {
|
||||||
@@ -3054,7 +3048,7 @@ void _attachSocketStreamingHandlers({
|
|||||||
if (line is String) {
|
if (line is String) {
|
||||||
final s = line.trim();
|
final s = line.trim();
|
||||||
// Any socket line is activity
|
// Any socket line is activity
|
||||||
_socketWatchdog.ping();
|
socketWatchdog.ping();
|
||||||
if (s == '[DONE]' || s == 'DONE') {
|
if (s == '[DONE]' || s == 'DONE') {
|
||||||
try {
|
try {
|
||||||
socketService.offEvent(channel);
|
socketService.offEvent(channel);
|
||||||
@@ -3072,7 +3066,7 @@ void _attachSocketStreamingHandlers({
|
|||||||
);
|
);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
ref.read(chatMessagesProvider.notifier).finishStreaming();
|
ref.read(chatMessagesProvider.notifier).finishStreaming();
|
||||||
_socketWatchdog.stop();
|
socketWatchdog.stop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (s.startsWith('data:')) {
|
if (s.startsWith('data:')) {
|
||||||
@@ -3094,7 +3088,7 @@ void _attachSocketStreamingHandlers({
|
|||||||
);
|
);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
ref.read(chatMessagesProvider.notifier).finishStreaming();
|
ref.read(chatMessagesProvider.notifier).finishStreaming();
|
||||||
_socketWatchdog.stop();
|
socketWatchdog.stop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -3118,9 +3112,7 @@ void _attachSocketStreamingHandlers({
|
|||||||
final exists =
|
final exists =
|
||||||
(msgs.isNotEmpty) &&
|
(msgs.isNotEmpty) &&
|
||||||
RegExp(
|
RegExp(
|
||||||
r'<details\s+type=\"tool_calls\"[^>]*\bname=\"' +
|
'<details\\s+type="tool_calls"[^>]*\\bname="${RegExp.escape(name)}"',
|
||||||
RegExp.escape(name) +
|
|
||||||
r'\"',
|
|
||||||
multiLine: true,
|
multiLine: true,
|
||||||
).hasMatch(msgs.last.content);
|
).hasMatch(msgs.last.content);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
@@ -3157,13 +3149,13 @@ void _attachSocketStreamingHandlers({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (line is Map) {
|
} else if (line is Map) {
|
||||||
_socketWatchdog.ping();
|
socketWatchdog.ping();
|
||||||
if (line['done'] == true) {
|
if (line['done'] == true) {
|
||||||
try {
|
try {
|
||||||
socketService.offEvent(channel);
|
socketService.offEvent(channel);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
ref.read(chatMessagesProvider.notifier).finishStreaming();
|
ref.read(chatMessagesProvider.notifier).finishStreaming();
|
||||||
_socketWatchdog.stop();
|
socketWatchdog.stop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3172,7 +3164,7 @@ void _attachSocketStreamingHandlers({
|
|||||||
|
|
||||||
socketService.onEvent(channel, handler);
|
socketService.onEvent(channel, handler);
|
||||||
// Start activity watchdog now that handler is attached
|
// Start activity watchdog now that handler is attached
|
||||||
_socketWatchdog.ping();
|
socketWatchdog.ping();
|
||||||
}
|
}
|
||||||
|
|
||||||
void chatHandler(Map<String, dynamic> ev) {
|
void chatHandler(Map<String, dynamic> ev) {
|
||||||
@@ -3198,9 +3190,7 @@ void _attachSocketStreamingHandlers({
|
|||||||
final exists =
|
final exists =
|
||||||
(msgs.isNotEmpty) &&
|
(msgs.isNotEmpty) &&
|
||||||
RegExp(
|
RegExp(
|
||||||
r'<details\s+type=\"tool_calls\"[^>]*\bname=\"' +
|
'<details\\s+type="tool_calls"[^>]*\\bname="${RegExp.escape(name)}"',
|
||||||
RegExp.escape(name) +
|
|
||||||
r'\"',
|
|
||||||
multiLine: true,
|
multiLine: true,
|
||||||
).hasMatch(msgs.last.content);
|
).hasMatch(msgs.last.content);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
@@ -3235,9 +3225,7 @@ void _attachSocketStreamingHandlers({
|
|||||||
final exists =
|
final exists =
|
||||||
(msgs.isNotEmpty) &&
|
(msgs.isNotEmpty) &&
|
||||||
RegExp(
|
RegExp(
|
||||||
r'<details\s+type=\"tool_calls\"[^>]*\bname=\"' +
|
'<details\\s+type="tool_calls"[^>]*\\bname="${RegExp.escape(name)}"',
|
||||||
RegExp.escape(name) +
|
|
||||||
r'\"',
|
|
||||||
multiLine: true,
|
multiLine: true,
|
||||||
).hasMatch(msgs.last.content);
|
).hasMatch(msgs.last.content);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
@@ -3267,7 +3255,7 @@ void _attachSocketStreamingHandlers({
|
|||||||
socketService.offChatEvents();
|
socketService.offChatEvents();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
try {
|
try {
|
||||||
_socketWatchdog.stop();
|
socketWatchdog.stop();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
try {
|
try {
|
||||||
unawaited(
|
unawaited(
|
||||||
@@ -3421,7 +3409,7 @@ void _attachSocketStreamingHandlers({
|
|||||||
socketService.onChatEvents(chatHandler);
|
socketService.onChatEvents(chatHandler);
|
||||||
socketService.onChannelEvents(channelEventsHandler);
|
socketService.onChannelEvents(channelEventsHandler);
|
||||||
// Start activity watchdog for chat/channel events
|
// Start activity watchdog for chat/channel events
|
||||||
_socketWatchdog.ping();
|
socketWatchdog.ping();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Tool Servers (OpenAPI) Helpers ==========
|
// ========== Tool Servers (OpenAPI) Helpers ==========
|
||||||
@@ -3495,8 +3483,9 @@ Map<String, dynamic>? _resolveRef(
|
|||||||
final section = components?[type];
|
final section = components?[type];
|
||||||
if (section is Map<String, dynamic>) {
|
if (section is Map<String, dynamic>) {
|
||||||
final schema = section[name];
|
final schema = section[name];
|
||||||
if (schema is Map<String, dynamic>)
|
if (schema is Map<String, dynamic>) {
|
||||||
return Map<String, dynamic>.from(schema);
|
return Map<String, dynamic>.from(schema);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -3515,12 +3504,14 @@ Map<String, dynamic> _resolveSchemaSimple(
|
|||||||
final out = <String, dynamic>{};
|
final out = <String, dynamic>{};
|
||||||
if (type is String) {
|
if (type is String) {
|
||||||
out['type'] = type;
|
out['type'] = type;
|
||||||
if (schema['description'] != null)
|
if (schema['description'] != null) {
|
||||||
out['description'] = schema['description'];
|
out['description'] = schema['description'];
|
||||||
|
}
|
||||||
if (type == 'object') {
|
if (type == 'object') {
|
||||||
out['properties'] = <String, dynamic>{};
|
out['properties'] = <String, dynamic>{};
|
||||||
if (schema['required'] is List)
|
if (schema['required'] is List) {
|
||||||
out['required'] = List.from(schema['required']);
|
out['required'] = List.from(schema['required']);
|
||||||
|
}
|
||||||
final props = schema['properties'];
|
final props = schema['properties'];
|
||||||
if (props is Map<String, dynamic>) {
|
if (props is Map<String, dynamic>) {
|
||||||
props.forEach((k, v) {
|
props.forEach((k, v) {
|
||||||
|
|||||||
@@ -557,15 +557,6 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement select all functionality when needed
|
|
||||||
// void _selectAllMessages() {
|
|
||||||
// final messages = ref.read(chatMessagesProvider);
|
|
||||||
// setState(() {
|
|
||||||
// _selectedMessageIds.clear();
|
|
||||||
// _selectedMessageIds.addAll(messages.map((m) => m.id));
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
void _clearSelection() {
|
void _clearSelection() {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedMessageIds.clear();
|
_selectedMessageIds.clear();
|
||||||
@@ -752,8 +743,6 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
modelName: displayModelName,
|
modelName: displayModelName,
|
||||||
onCopy: () => _copyMessage(message.content),
|
onCopy: () => _copyMessage(message.content),
|
||||||
onRegenerate: () => _regenerateMessage(message),
|
onRegenerate: () => _regenerateMessage(message),
|
||||||
onLike: () => _likeMessage(message),
|
|
||||||
onDislike: () => _dislikeMessage(message),
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
messageWidget = assistant.AssistantMessageWidget(
|
messageWidget = assistant.AssistantMessageWidget(
|
||||||
@@ -763,8 +752,6 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
modelName: displayModelName,
|
modelName: displayModelName,
|
||||||
onCopy: () => _copyMessage(message.content),
|
onCopy: () => _copyMessage(message.content),
|
||||||
onRegenerate: () => _regenerateMessage(message),
|
onRegenerate: () => _regenerateMessage(message),
|
||||||
onLike: () => _likeMessage(message),
|
|
||||||
onDislike: () => _dislikeMessage(message),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -840,14 +827,6 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
|
|
||||||
// Inline editing handled by UserMessageBubble. Dialog flow removed.
|
// Inline editing handled by UserMessageBubble. Dialog flow removed.
|
||||||
|
|
||||||
void _likeMessage(dynamic message) {
|
|
||||||
// TODO: Implement message liking
|
|
||||||
}
|
|
||||||
|
|
||||||
void _dislikeMessage(dynamic message) {
|
|
||||||
// TODO: Implement message disliking
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEmptyState(ThemeData theme) {
|
Widget _buildEmptyState(ThemeData theme) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final currentUserAsync = ref.watch(currentUserProvider);
|
final currentUserAsync = ref.watch(currentUserProvider);
|
||||||
@@ -1575,13 +1554,6 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement chat options when needed
|
|
||||||
// void _showChatOptions() {
|
|
||||||
// ScaffoldMessenger.of(
|
|
||||||
// context,
|
|
||||||
// ).showSnackBar(const SnackBar(content: Text('Chat options coming soon!')));
|
|
||||||
// }
|
|
||||||
|
|
||||||
void _deleteSelectedMessages() {
|
void _deleteSelectedMessages() {
|
||||||
final selectedMessages = _getSelectedMessages();
|
final selectedMessages = _getSelectedMessages();
|
||||||
if (selectedMessages.isEmpty) return;
|
if (selectedMessages.isEmpty) return;
|
||||||
@@ -1594,7 +1566,6 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
isDestructive: true,
|
isDestructive: true,
|
||||||
).then((confirmed) async {
|
).then((confirmed) async {
|
||||||
if (confirmed == true) {
|
if (confirmed == true) {
|
||||||
// TODO: Implement message removal
|
|
||||||
// for (final selectedMessage in selectedMessages) {
|
// for (final selectedMessage in selectedMessages) {
|
||||||
// ref.read(chatMessagesProvider.notifier).removeMessage(selectedMessage.id);
|
// ref.read(chatMessagesProvider.notifier).removeMessage(selectedMessage.id);
|
||||||
// }
|
// }
|
||||||
|
|||||||
@@ -181,10 +181,12 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
|||||||
final isExpanded = _expandedToolIds.contains(tc.id);
|
final isExpanded = _expandedToolIds.contains(tc.id);
|
||||||
final theme = context.conduitTheme;
|
final theme = context.conduitTheme;
|
||||||
|
|
||||||
String _pretty(dynamic v, {int max = 1200}) {
|
String pretty(dynamic v, {int max = 1200}) {
|
||||||
try {
|
try {
|
||||||
final pretty = const JsonEncoder.withIndent(' ').convert(v);
|
final formatted = const JsonEncoder.withIndent(' ').convert(v);
|
||||||
return pretty.length > max ? '${pretty.substring(0, max)}\n…' : pretty;
|
return formatted.length > max
|
||||||
|
? '${formatted.substring(0, max)}\n…'
|
||||||
|
: formatted;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
final s = v?.toString() ?? '';
|
final s = v?.toString() ?? '';
|
||||||
return s.length > max ? '${s.substring(0, max)}…' : s;
|
return s.length > max ? '${s.substring(0, max)}…' : s;
|
||||||
@@ -233,7 +235,9 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
|||||||
),
|
),
|
||||||
const SizedBox(width: Spacing.xs),
|
const SizedBox(width: Spacing.xs),
|
||||||
Icon(
|
Icon(
|
||||||
tc.done ? Icons.build_circle_outlined : Icons.play_circle_outline,
|
tc.done
|
||||||
|
? Icons.build_circle_outlined
|
||||||
|
: Icons.play_circle_outline,
|
||||||
size: 14,
|
size: 14,
|
||||||
color: theme.buttonPrimary,
|
color: theme.buttonPrimary,
|
||||||
),
|
),
|
||||||
@@ -281,7 +285,7 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
|||||||
),
|
),
|
||||||
const SizedBox(height: Spacing.xxs),
|
const SizedBox(height: Spacing.xxs),
|
||||||
SelectableText(
|
SelectableText(
|
||||||
_pretty(tc.arguments),
|
pretty(tc.arguments),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: AppTypography.bodySmall,
|
fontSize: AppTypography.bodySmall,
|
||||||
color: theme.textSecondary,
|
color: theme.textSecondary,
|
||||||
@@ -303,7 +307,7 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
|||||||
),
|
),
|
||||||
const SizedBox(height: Spacing.xxs),
|
const SizedBox(height: Spacing.xxs),
|
||||||
SelectableText(
|
SelectableText(
|
||||||
_pretty(tc.result),
|
pretty(tc.result),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: AppTypography.bodySmall,
|
fontSize: AppTypography.bodySmall,
|
||||||
color: theme.textSecondary,
|
color: theme.textSecondary,
|
||||||
@@ -315,8 +319,9 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
crossFadeState:
|
crossFadeState: isExpanded
|
||||||
isExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst,
|
? CrossFadeState.showSecond
|
||||||
|
: CrossFadeState.showFirst,
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -331,7 +336,7 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
|||||||
// Determine if media (attachments or generated images) is rendered above.
|
// Determine if media (attachments or generated images) is rendered above.
|
||||||
final hasMediaAbove =
|
final hasMediaAbove =
|
||||||
(widget.message.attachmentIds?.isNotEmpty ?? false) ||
|
(widget.message.attachmentIds?.isNotEmpty ?? false) ||
|
||||||
(widget.message.files?.isNotEmpty ?? false);
|
(widget.message.files?.isNotEmpty ?? false);
|
||||||
bool firstToolSpacerAdded = false;
|
bool firstToolSpacerAdded = false;
|
||||||
int idx = 0;
|
int idx = 0;
|
||||||
for (final seg in _segments) {
|
for (final seg in _segments) {
|
||||||
@@ -363,7 +368,7 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool get _hasRenderableSegments {
|
bool get _hasRenderableSegments {
|
||||||
bool _textRenderable(String t) {
|
bool textRenderable(String t) {
|
||||||
String cleaned = t;
|
String cleaned = t;
|
||||||
// Hide tool_calls blocks entirely
|
// Hide tool_calls blocks entirely
|
||||||
cleaned = cleaned.replaceAll(
|
cleaned = cleaned.replaceAll(
|
||||||
@@ -398,7 +403,7 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
|||||||
if (seg.isTool && seg.toolCall != null) return true;
|
if (seg.isTool && seg.toolCall != null) return true;
|
||||||
if (seg.isReasoning && seg.reasoning != null) return true;
|
if (seg.isReasoning && seg.reasoning != null) return true;
|
||||||
final text = seg.text ?? '';
|
final text = seg.text ?? '';
|
||||||
if (_textRenderable(text)) return true;
|
if (textRenderable(text)) return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -507,7 +512,8 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: (widget.isStreaming &&
|
child:
|
||||||
|
(widget.isStreaming &&
|
||||||
!_hasRenderableSegments &&
|
!_hasRenderableSegments &&
|
||||||
_allowTypingIndicator)
|
_allowTypingIndicator)
|
||||||
? KeyedSubtree(
|
? KeyedSubtree(
|
||||||
@@ -566,8 +572,18 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
|||||||
);
|
);
|
||||||
// Remove raw <think>...</think> or <reasoning>...</reasoning> tags in text
|
// Remove raw <think>...</think> or <reasoning>...</reasoning> tags in text
|
||||||
cleaned = cleaned
|
cleaned = cleaned
|
||||||
.replaceAll(RegExp(r'<think>[\s\S]*?<\/think>', multiLine: true, dotAll: true), '')
|
.replaceAll(
|
||||||
.replaceAll(RegExp(r'<reasoning>[\s\S]*?<\/reasoning>', multiLine: true, dotAll: true), '');
|
RegExp(r'<think>[\s\S]*?<\/think>', multiLine: true, dotAll: true),
|
||||||
|
'',
|
||||||
|
)
|
||||||
|
.replaceAll(
|
||||||
|
RegExp(
|
||||||
|
r'<reasoning>[\s\S]*?<\/reasoning>',
|
||||||
|
multiLine: true,
|
||||||
|
dotAll: true,
|
||||||
|
),
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
|
||||||
// If there's an unclosed <details>, drop the tail to avoid raw tags.
|
// If there's an unclosed <details>, drop the tail to avoid raw tags.
|
||||||
final lastOpen = cleaned.lastIndexOf('<details');
|
final lastOpen = cleaned.lastIndexOf('<details');
|
||||||
@@ -699,7 +715,8 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
|||||||
maxWidth: 500,
|
maxWidth: 500,
|
||||||
maxHeight: 400,
|
maxHeight: 400,
|
||||||
),
|
),
|
||||||
disableAnimation: false, // Keep animations enabled to prevent black display
|
disableAnimation:
|
||||||
|
false, // Keep animations enabled to prevent black display
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -722,7 +739,8 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
|||||||
maxWidth: imageCount == 2 ? 245 : 160,
|
maxWidth: imageCount == 2 ? 245 : 160,
|
||||||
maxHeight: imageCount == 2 ? 245 : 160,
|
maxHeight: imageCount == 2 ? 245 : 160,
|
||||||
),
|
),
|
||||||
disableAnimation: false, // Keep animations enabled to prevent black display
|
disableAnimation:
|
||||||
|
false, // Keep animations enabled to prevent black display
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
),
|
||||||
@@ -764,13 +782,10 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
|||||||
|
|
||||||
Widget dot(Duration delay) {
|
Widget dot(Duration delay) {
|
||||||
return Container(
|
return Container(
|
||||||
width: dotSize,
|
width: dotSize,
|
||||||
height: dotSize,
|
height: dotSize,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(color: dotColor, shape: BoxShape.circle),
|
||||||
color: dotColor,
|
)
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.animate(onPlay: (controller) => controller.repeat())
|
.animate(onPlay: (controller) => controller.repeat())
|
||||||
.then(delay: delay)
|
.then(delay: delay)
|
||||||
.scale(
|
.scale(
|
||||||
@@ -816,13 +831,10 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
|||||||
|
|
||||||
Widget dot(Duration delay) {
|
Widget dot(Duration delay) {
|
||||||
return Container(
|
return Container(
|
||||||
width: dotSize,
|
width: dotSize,
|
||||||
height: dotSize,
|
height: dotSize,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(color: dotColor, shape: BoxShape.circle),
|
||||||
color: dotColor,
|
)
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.animate(onPlay: (controller) => controller.repeat())
|
.animate(onPlay: (controller) => controller.repeat())
|
||||||
.then(delay: delay)
|
.then(delay: delay)
|
||||||
.scale(
|
.scale(
|
||||||
@@ -859,8 +871,6 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Widget _buildActionButtons() {
|
Widget _buildActionButtons() {
|
||||||
final isErrorMessage =
|
final isErrorMessage =
|
||||||
widget.message.content.contains('⚠️') ||
|
widget.message.content.contains('⚠️') ||
|
||||||
@@ -914,7 +924,8 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
|||||||
String headerText() {
|
String headerText() {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final hasSummary = rc.summary.isNotEmpty;
|
final hasSummary = rc.summary.isNotEmpty;
|
||||||
final isThinkingSummary = rc.summary.trim().toLowerCase() == 'thinking…' ||
|
final isThinkingSummary =
|
||||||
|
rc.summary.trim().toLowerCase() == 'thinking…' ||
|
||||||
rc.summary.trim().toLowerCase() == 'thinking...';
|
rc.summary.trim().toLowerCase() == 'thinking...';
|
||||||
if (widget.isStreaming) {
|
if (widget.isStreaming) {
|
||||||
return hasSummary ? rc.summary : l10n.thinking;
|
return hasSummary ? rc.summary : l10n.thinking;
|
||||||
@@ -1012,8 +1023,9 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
crossFadeState:
|
crossFadeState: isExpanded
|
||||||
isExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst,
|
? CrossFadeState.showSecond
|
||||||
|
: CrossFadeState.showFirst,
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -131,7 +131,9 @@ class _EnhancedAttachmentState extends ConsumerState<EnhancedAttachment> {
|
|||||||
if (path == null) return;
|
if (path == null) return;
|
||||||
final filename = (_fileInfo?['filename'] ?? _fileInfo?['name'] ?? 'file')
|
final filename = (_fileInfo?['filename'] ?? _fileInfo?['name'] ?? 'file')
|
||||||
.toString();
|
.toString();
|
||||||
await Share.shareXFiles([XFile(path, name: filename)]);
|
await SharePlus.instance.share(
|
||||||
|
ShareParams(files: [XFile(path, name: filename)]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _fileIconFor(String filename) {
|
String _fileIconFor(String filename) {
|
||||||
|
|||||||
@@ -151,7 +151,6 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
final FocusNode _focusNode = FocusNode();
|
final FocusNode _focusNode = FocusNode();
|
||||||
bool _isRecording = false;
|
bool _isRecording = false;
|
||||||
bool _isExpanded = true; // Start expanded for better UX
|
bool _isExpanded = true; // Start expanded for better UX
|
||||||
// TODO: Implement voice input functionality
|
|
||||||
// final String _voiceInputText = '';
|
// final String _voiceInputText = '';
|
||||||
bool _hasText = false; // track locally without rebuilding on each keystroke
|
bool _hasText = false; // track locally without rebuilding on each keystroke
|
||||||
StreamSubscription<String>? _voiceStreamSubscription;
|
StreamSubscription<String>? _voiceStreamSubscription;
|
||||||
@@ -414,8 +413,6 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final bool showPlaceholder =
|
|
||||||
!_hasText && !_focusNode.hasFocus && !_isRecording;
|
|
||||||
final Brightness brightness = Theme.of(context).brightness;
|
final Brightness brightness = Theme.of(context).brightness;
|
||||||
final Color outlineColor = (_focusNode.hasFocus || _hasText)
|
final Color outlineColor = (_focusNode.hasFocus || _hasText)
|
||||||
? context.conduitTheme.inputBorderFocused.withValues(alpha: 0.6)
|
? context.conduitTheme.inputBorderFocused.withValues(alpha: 0.6)
|
||||||
@@ -425,16 +422,6 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
);
|
);
|
||||||
final Color composerSurface = context.conduitTheme.inputBackground;
|
final Color composerSurface = context.conduitTheme.inputBackground;
|
||||||
final Color placeholderColor = context.conduitTheme.inputPlaceholder;
|
final Color placeholderColor = context.conduitTheme.inputPlaceholder;
|
||||||
final Color badgeBackground = showPlaceholder
|
|
||||||
? placeholderColor.withValues(alpha: 0.12)
|
|
||||||
: composerSurface.withValues(alpha: 0.3);
|
|
||||||
final Color badgeBorder = showPlaceholder
|
|
||||||
? Colors.transparent
|
|
||||||
: outlineColor.withValues(alpha: 0.35);
|
|
||||||
final Color badgeIconColor = showPlaceholder
|
|
||||||
? placeholderColor
|
|
||||||
: context.conduitTheme.textPrimary.withValues(alpha: 0.75);
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
// Transparent wrapper so rounded corners are visible against page background
|
// Transparent wrapper so rounded corners are visible against page background
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
@@ -679,21 +666,21 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!_isExpanded) ...[
|
if (!_isExpanded) ...[
|
||||||
const SizedBox(width: Spacing.sm),
|
const SizedBox(width: Spacing.sm),
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (voiceAvailable) ...[
|
if (voiceAvailable) ...[
|
||||||
_buildVoiceButton(voiceAvailable),
|
_buildVoiceButton(voiceAvailable),
|
||||||
const SizedBox(width: Spacing.xs),
|
const SizedBox(width: Spacing.xs),
|
||||||
],
|
|
||||||
_buildPrimaryButton(
|
|
||||||
_hasText,
|
|
||||||
isGenerating,
|
|
||||||
stopGeneration,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
_buildPrimaryButton(
|
||||||
|
_hasText,
|
||||||
|
isGenerating,
|
||||||
|
stopGeneration,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -1017,7 +1004,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
|
|
||||||
// Append tools button at the end (always visible)
|
// Append tools button at the end (always visible)
|
||||||
|
|
||||||
rowChildren..add(
|
rowChildren.add(
|
||||||
_buildIconButton(
|
_buildIconButton(
|
||||||
icon: Platform.isIOS
|
icon: Platform.isIOS
|
||||||
? CupertinoIcons.wrench
|
? CupertinoIcons.wrench
|
||||||
@@ -1605,6 +1592,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
if (!widget.enabled) return;
|
if (!widget.enabled) return;
|
||||||
try {
|
try {
|
||||||
final ok = await _voiceService.initialize();
|
final ok = await _voiceService.initialize();
|
||||||
|
if (!mounted) return;
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
_showVoiceUnavailable(
|
_showVoiceUnavailable(
|
||||||
AppLocalizations.of(context)?.errorMessage ??
|
AppLocalizations.of(context)?.errorMessage ??
|
||||||
@@ -1614,6 +1602,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
}
|
}
|
||||||
// Centralized permission + start
|
// Centralized permission + start
|
||||||
final stream = await _voiceService.beginListening();
|
final stream = await _voiceService.beginListening();
|
||||||
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_isRecording = true;
|
_isRecording = true;
|
||||||
_baseTextAtStart = _controller.text;
|
_baseTextAtStart = _controller.text;
|
||||||
|
|||||||
@@ -819,15 +819,18 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
String folderId,
|
String folderId,
|
||||||
String folderName,
|
String folderName,
|
||||||
) async {
|
) async {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final confirmed = await ThemedDialogs.confirm(
|
final confirmed = await ThemedDialogs.confirm(
|
||||||
context,
|
context,
|
||||||
title: AppLocalizations.of(context)!.deleteFolderTitle,
|
title: l10n.deleteFolderTitle,
|
||||||
message: AppLocalizations.of(context)!.deleteFolderMessage,
|
message: l10n.deleteFolderMessage,
|
||||||
confirmText: AppLocalizations.of(context)!.delete,
|
confirmText: l10n.delete,
|
||||||
isDestructive: true,
|
isDestructive: true,
|
||||||
);
|
);
|
||||||
|
if (!mounted) return;
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
final deleteFolderError = l10n.failedToDeleteFolder;
|
||||||
try {
|
try {
|
||||||
final api = ref.read(apiServiceProvider);
|
final api = ref.read(apiServiceProvider);
|
||||||
if (api == null) throw Exception('No API service');
|
if (api == null) throw Exception('No API service');
|
||||||
@@ -837,16 +840,13 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
ref.invalidate(conversationsProvider);
|
ref.invalidate(conversationsProvider);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
UiUtils.showMessage(
|
UiUtils.showMessage(this.context, deleteFolderError, isError: true);
|
||||||
this.context,
|
|
||||||
AppLocalizations.of(context)!.failedToDeleteFolder,
|
|
||||||
isError: true,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildUnfileDropTarget() {
|
Widget _buildUnfileDropTarget() {
|
||||||
final theme = context.conduitTheme;
|
final theme = context.conduitTheme;
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final isHover = _dragHoverFolderId == '__UNFILE__';
|
final isHover = _dragHoverFolderId == '__UNFILE__';
|
||||||
return DragTarget<_DragConversationData>(
|
return DragTarget<_DragConversationData>(
|
||||||
onWillAcceptWithDetails: (details) {
|
onWillAcceptWithDetails: (details) {
|
||||||
@@ -874,11 +874,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
UiUtils.showMessage(
|
UiUtils.showMessage(context, l10n.failedToMoveChat, isError: true);
|
||||||
context,
|
|
||||||
AppLocalizations.of(context)!.failedToMoveChat,
|
|
||||||
isError: true,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1149,14 +1145,14 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
final dynamic authUser = ref.watch(authUserProvider);
|
final dynamic authUser = ref.watch(authUserProvider);
|
||||||
final user = userFromProfile ?? authUser;
|
final user = userFromProfile ?? authUser;
|
||||||
|
|
||||||
String _initial(String name) {
|
String initialFor(String name) {
|
||||||
if (name.isEmpty) return 'U';
|
if (name.isEmpty) return 'U';
|
||||||
final ch = name.characters.first;
|
final ch = name.characters.first;
|
||||||
return ch.toUpperCase();
|
return ch.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
final displayName = deriveUserDisplayName(user);
|
final displayName = deriveUserDisplayName(user);
|
||||||
final initial = _initial(displayName);
|
final initial = initialFor(displayName);
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(Spacing.sm, 0, Spacing.sm, Spacing.sm),
|
padding: const EdgeInsets.fromLTRB(Spacing.sm, 0, Spacing.sm, Spacing.sm),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -1273,13 +1269,16 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
onTap: () async {
|
onTap: () async {
|
||||||
HapticFeedback.lightImpact();
|
HapticFeedback.lightImpact();
|
||||||
Navigator.pop(sheetContext);
|
Navigator.pop(sheetContext);
|
||||||
|
final pinErrorMessage = AppLocalizations.of(
|
||||||
|
context,
|
||||||
|
)!.failedToUpdatePin;
|
||||||
try {
|
try {
|
||||||
await chat.pinConversation(ref, conv.id, !isPinned);
|
await chat.pinConversation(ref, conv.id, !isPinned);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
UiUtils.showMessage(
|
UiUtils.showMessage(
|
||||||
this.context,
|
this.context,
|
||||||
AppLocalizations.of(context)!.failedToUpdatePin,
|
pinErrorMessage,
|
||||||
isError: true,
|
isError: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1305,13 +1304,16 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
onTap: () async {
|
onTap: () async {
|
||||||
HapticFeedback.lightImpact();
|
HapticFeedback.lightImpact();
|
||||||
Navigator.pop(sheetContext);
|
Navigator.pop(sheetContext);
|
||||||
|
final archiveErrorMessage = AppLocalizations.of(
|
||||||
|
context,
|
||||||
|
)!.failedToUpdateArchive;
|
||||||
try {
|
try {
|
||||||
await chat.archiveConversation(ref, conv.id, !isArchived);
|
await chat.archiveConversation(ref, conv.id, !isArchived);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
UiUtils.showMessage(
|
UiUtils.showMessage(
|
||||||
this.context,
|
this.context,
|
||||||
AppLocalizations.of(context)!.failedToUpdateArchive,
|
archiveErrorMessage,
|
||||||
isError: true,
|
isError: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1360,18 +1362,20 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
String conversationId,
|
String conversationId,
|
||||||
String currentTitle,
|
String currentTitle,
|
||||||
) async {
|
) async {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final newName = await ThemedDialogs.promptTextInput(
|
final newName = await ThemedDialogs.promptTextInput(
|
||||||
context,
|
context,
|
||||||
title: AppLocalizations.of(context)!.renameChat,
|
title: l10n.renameChat,
|
||||||
hintText: AppLocalizations.of(context)!.enterChatName,
|
hintText: l10n.enterChatName,
|
||||||
initialValue: currentTitle,
|
initialValue: currentTitle,
|
||||||
confirmText: AppLocalizations.of(context)!.save,
|
confirmText: l10n.save,
|
||||||
cancelText: AppLocalizations.of(context)!.cancel,
|
cancelText: l10n.cancel,
|
||||||
);
|
);
|
||||||
|
if (!mounted) return;
|
||||||
if (newName == null) return;
|
if (newName == null) return;
|
||||||
if (newName.isEmpty || newName == currentTitle) return;
|
if (newName.isEmpty || newName == currentTitle) return;
|
||||||
|
|
||||||
|
final renameError = l10n.failedToRenameChat;
|
||||||
try {
|
try {
|
||||||
final api = ref.read(apiServiceProvider);
|
final api = ref.read(apiServiceProvider);
|
||||||
if (api == null) throw Exception('No API service');
|
if (api == null) throw Exception('No API service');
|
||||||
@@ -1387,11 +1391,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
UiUtils.showMessage(
|
UiUtils.showMessage(this.context, renameError, isError: true);
|
||||||
this.context,
|
|
||||||
AppLocalizations.of(context)!.failedToRenameChat,
|
|
||||||
isError: true,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1399,15 +1399,18 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
BuildContext context,
|
BuildContext context,
|
||||||
String conversationId,
|
String conversationId,
|
||||||
) async {
|
) async {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final confirmed = await ThemedDialogs.confirm(
|
final confirmed = await ThemedDialogs.confirm(
|
||||||
context,
|
context,
|
||||||
title: AppLocalizations.of(context)!.deleteChatTitle,
|
title: l10n.deleteChatTitle,
|
||||||
message: AppLocalizations.of(context)!.deleteChatMessage,
|
message: l10n.deleteChatMessage,
|
||||||
confirmText: AppLocalizations.of(context)!.delete,
|
confirmText: l10n.delete,
|
||||||
isDestructive: true,
|
isDestructive: true,
|
||||||
);
|
);
|
||||||
|
if (!mounted) return;
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
final deleteError = l10n.failedToDeleteChat;
|
||||||
try {
|
try {
|
||||||
final api = ref.read(apiServiceProvider);
|
final api = ref.read(apiServiceProvider);
|
||||||
if (api == null) throw Exception('No API service');
|
if (api == null) throw Exception('No API service');
|
||||||
@@ -1422,11 +1425,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
ref.invalidate(conversationsProvider);
|
ref.invalidate(conversationsProvider);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
UiUtils.showMessage(
|
UiUtils.showMessage(this.context, deleteError, isError: true);
|
||||||
this.context,
|
|
||||||
AppLocalizations.of(context)!.failedToDeleteChat,
|
|
||||||
isError: true,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,22 +7,24 @@ class MeasureSize extends SingleChildRenderObjectWidget {
|
|||||||
final OnWidgetSizeChange onChange;
|
final OnWidgetSizeChange onChange;
|
||||||
|
|
||||||
const MeasureSize({super.key, required this.onChange, required Widget child})
|
const MeasureSize({super.key, required this.onChange, required Widget child})
|
||||||
: super(child: child);
|
: super(child: child);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RenderObject createRenderObject(BuildContext context) {
|
RenderObject createRenderObject(BuildContext context) {
|
||||||
return _MeasureSizeRenderObject(onChange);
|
return MeasureSizeRenderObject(onChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void updateRenderObject(
|
void updateRenderObject(
|
||||||
BuildContext context, covariant _MeasureSizeRenderObject renderObject) {
|
BuildContext context,
|
||||||
|
covariant MeasureSizeRenderObject renderObject,
|
||||||
|
) {
|
||||||
renderObject.onChange = onChange;
|
renderObject.onChange = onChange;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MeasureSizeRenderObject extends RenderProxyBox {
|
class MeasureSizeRenderObject extends RenderProxyBox {
|
||||||
_MeasureSizeRenderObject(this.onChange);
|
MeasureSizeRenderObject(this.onChange);
|
||||||
|
|
||||||
OnWidgetSizeChange onChange;
|
OnWidgetSizeChange onChange;
|
||||||
Size? _oldSize;
|
Size? _oldSize;
|
||||||
|
|||||||
Reference in New Issue
Block a user