refactor: streamline title and image generation handling in chat message processing

This commit is contained in:
cogwheel0
2025-08-25 22:14:40 +05:30
parent 9eeb6d4802
commit 494427dd04
2 changed files with 35 additions and 165 deletions

View File

@@ -531,6 +531,34 @@ Future<void> _sendMessageInternal(
// We'll add the assistant message placeholder after we get the message ID from the API (or immediately in reviewer mode)
// Immediately trigger title generation after user message is sent (first turn only)
try {
final currentConversation = ref.read(activeConversationProvider);
if (currentConversation != null &&
currentConversation.title == 'New Chat') {
final currentMessages = ref.read(chatMessagesProvider);
if (currentMessages.length == 1 && currentMessages.first.role == 'user') {
final List<Map<String, dynamic>> formatted = [
{
'id': currentMessages.first.id,
'role': currentMessages.first.role,
'content': currentMessages.first.content,
'timestamp':
currentMessages.first.timestamp.millisecondsSinceEpoch ~/ 1000,
},
];
_triggerTitleGeneration(
ref,
currentConversation.id,
formatted,
selectedModel.id,
);
}
}
} catch (e) {
// Silent fail for early title generation
}
// Reviewer mode: simulate a response locally and return
if (reviewerMode) {
// Add assistant message placeholder
@@ -1229,7 +1257,7 @@ Future<void> _sendMessageInternal(
// Continue even if this fails - it's non-critical
}
// Fetch the latest conversation state without waiting for title generation
// Fetch the latest conversation state
try {
// Quick fetch to get the current state - no waiting for title generation
@@ -1243,19 +1271,6 @@ Future<void> _sendMessageInternal(
updatedConv.title != 'New Chat' &&
updatedConv.title.isNotEmpty;
// If title is still "New Chat" and this is the first exchange, trigger title generation
if (messages.length <= 2 && updatedConv.title == 'New Chat') {
debugPrint(
'DEBUG: Triggering title generation for conversation ${activeConversation.id}',
);
_triggerTitleGeneration(
ref,
activeConversation.id,
formattedMessages,
selectedModel.id,
);
}
// Always combine current local messages with updated server content
final currentMessages = ref.read(chatMessagesProvider);
final serverMessages = updatedConv.messages;
@@ -1378,11 +1393,7 @@ Future<void> _sendMessageInternal(
}
// Streaming already marked as complete when stream ended
// Start background title check for first message exchanges
if (messages.length <= 2 && updatedConv.title == 'New Chat') {
_checkForTitleInBackground(ref, activeConversation.id);
}
// Removed post-assistant title trigger/background check; handled right after user message
} catch (e) {
// Streaming already marked as complete when stream ended
}
@@ -1399,119 +1410,7 @@ Future<void> _sendMessageInternal(
await Future.delayed(const Duration(milliseconds: 100));
await _saveConversationToServer(ref);
// If image generation is enabled, trigger image generation with the user's prompt
if (imageGenerationEnabled) {
try {
final imageResponse = await api.generateImage(prompt: message);
// Extract image URLs or base64 data URIs from response
List<Map<String, dynamic>> extractGeneratedFiles(dynamic resp) {
final results = <Map<String, dynamic>>[];
// If it's already a list (e.g., list of URLs or file maps)
if (resp is List) {
for (final item in resp) {
if (item is String && item.isNotEmpty) {
results.add({'type': 'image', 'url': item});
} else if (item is Map) {
final url = item['url'];
final b64 = item['b64_json'] ?? item['b64'];
if (url is String && url.isNotEmpty) {
results.add({'type': 'image', 'url': url});
} else if (b64 is String && b64.isNotEmpty) {
results.add({
'type': 'image',
'url': 'data:image/png;base64,$b64',
});
}
}
}
return results;
}
if (resp is! Map) {
return results;
}
// Common patterns: { data: [ { url }, { b64_json } ] }
final data = resp['data'];
if (data is List) {
for (final item in data) {
if (item is Map) {
final url = item['url'];
final b64 = item['b64_json'] ?? item['b64'];
if (url is String && url.isNotEmpty) {
results.add({'type': 'image', 'url': url});
} else if (b64 is String && b64.isNotEmpty) {
// Default to PNG for base64 images
results.add({
'type': 'image',
'url': 'data:image/png;base64,$b64',
});
}
} else if (item is String && item.isNotEmpty) {
// Some servers may return a list of URLs
results.add({'type': 'image', 'url': item});
}
}
}
// Alternative patterns
final images = resp['images'];
if (images is List) {
for (final item in images) {
if (item is String && item.isNotEmpty) {
results.add({'type': 'image', 'url': item});
} else if (item is Map) {
final url = item['url'];
final b64 = item['b64_json'] ?? item['b64'];
if (url is String && url.isNotEmpty) {
results.add({'type': 'image', 'url': url});
} else if (b64 is String && b64.isNotEmpty) {
results.add({
'type': 'image',
'url': 'data:image/png;base64,$b64',
});
}
}
}
}
// Single fields
final singleUrl = resp['url'];
if (singleUrl is String && singleUrl.isNotEmpty) {
results.add({'type': 'image', 'url': singleUrl});
}
final singleB64 = resp['b64_json'] ?? resp['b64'];
if (singleB64 is String && singleB64.isNotEmpty) {
results.add({
'type': 'image',
'url': 'data:image/png;base64,$singleB64',
});
}
return results;
}
final generatedFiles = extractGeneratedFiles(imageResponse);
if (generatedFiles.isNotEmpty) {
// Attach images to the last assistant message
ref
.read(chatMessagesProvider.notifier)
.updateLastMessageWithFunction((ChatMessage m) {
final currentFiles = m.files ?? <Map<String, dynamic>>[];
return m.copyWith(
files: [...currentFiles, ...generatedFiles],
);
});
// Save updated conversation with images
await _saveConversationToServer(ref);
}
} catch (e) {
// Handle image generation errors silently
}
}
// Removed post-assistant image generation; images are handled immediately after user message
},
onError: (error) {
// Mark streaming as complete on error

View File

@@ -1144,42 +1144,13 @@ class _ChatPageState extends ConsumerState<ChatPage> {
mainAxisSize: MainAxisSize.min,
children: [
Transform.translate(
offset: const Offset(-6, 0),
child: Center(
offset: const Offset(0, 0),
child: SizedBox(
height: 28,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Opacity(
opacity: 0.0,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: Spacing.xs,
vertical: Spacing.xxs,
),
decoration: BoxDecoration(
color: context
.conduitTheme
.surfaceBackground
.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(
AppBorderRadius.badge,
),
border: Border.all(
color:
context.conduitTheme.dividerColor,
width: BorderWidth.thin,
),
),
child: Icon(
Platform.isIOS
? CupertinoIcons.chevron_down
: Icons.keyboard_arrow_down,
size: IconSize.small,
),
),
),
const SizedBox(width: Spacing.xs),
Flexible(
child: Text(
'Choose Model',