feat: image generation

This commit is contained in:
cogwheel0
2025-08-21 14:37:49 +05:30
parent 0c4f323814
commit e63c57d1fe
4 changed files with 466 additions and 148 deletions

View File

@@ -54,7 +54,7 @@ class ChatMessagesNotifier extends StateNotifier<List<ChatMessage>> {
'DEBUG: Loading ${next.messages.length} messages for conversation ${next.id}',
);
state = next.messages;
// Update selected model if conversation has a different model
_updateModelForConversation(next);
} else {
@@ -76,39 +76,43 @@ class ChatMessagesNotifier extends StateNotifier<List<ChatMessage>> {
}
Future<void> _updateModelForConversation(Conversation conversation) async {
debugPrint('DEBUG: _updateModelForConversation called for conversation ${conversation.id}');
debugPrint(
'DEBUG: _updateModelForConversation called for conversation ${conversation.id}',
);
// Check if conversation has a model specified
if (conversation.model == null || conversation.model!.isEmpty) {
debugPrint('DEBUG: Conversation has no model specified');
return;
}
debugPrint('DEBUG: Conversation model: ${conversation.model}');
final currentSelectedModel = _ref.read(selectedModelProvider);
// If the conversation's model is different from the currently selected one
if (currentSelectedModel?.id != conversation.model) {
debugPrint(
'DEBUG: Conversation model (${conversation.model}) differs from selected model (${currentSelectedModel?.id})',
);
// Get available models to find the matching one
try {
final models = await _ref.read(modelsProvider.future);
debugPrint('DEBUG: Available models count: ${models.length}');
if (models.isEmpty) {
debugPrint('DEBUG: No models available, cannot update selected model');
debugPrint(
'DEBUG: No models available, cannot update selected model',
);
return;
}
// Look for exact match first
final conversationModel = models.where(
(model) => model.id == conversation.model,
).firstOrNull;
final conversationModel = models
.where((model) => model.id == conversation.model)
.firstOrNull;
if (conversationModel != null) {
// Update the selected model
_ref.read(selectedModelProvider.notifier).state = conversationModel;
@@ -163,17 +167,16 @@ class ChatMessagesNotifier extends StateNotifier<List<ChatMessage>> {
lastMessage.copyWith(content: content),
];
}
void updateLastMessageWithFunction(ChatMessage Function(ChatMessage) updater) {
void updateLastMessageWithFunction(
ChatMessage Function(ChatMessage) updater,
) {
if (state.isEmpty) return;
final lastMessage = state.last;
if (lastMessage.role != 'assistant') return;
state = [
...state.sublist(0, state.length - 1),
updater(lastMessage),
];
state = [...state.sublist(0, state.length - 1), updater(lastMessage)];
}
void appendToLastMessage(String content) {
@@ -286,6 +289,9 @@ final availableToolsProvider = StateProvider<List<String>>((ref) => []);
// Web search enabled state for API-based web search
final webSearchEnabledProvider = StateProvider<bool>((ref) => false);
// Image generation enabled state - behaves like web search
final imageGenerationEnabledProvider = StateProvider<bool>((ref) => false);
// Vision capable models provider
final visionCapableModelsProvider = StateProvider<List<String>>((ref) {
final selectedModel = ref.watch(selectedModelProvider);
@@ -383,8 +389,10 @@ Future<void> regenerateMessage(
String userMessageContent,
List<String>? attachments,
) async {
debugPrint('DEBUG: regenerateMessage called with content: $userMessageContent');
debugPrint(
'DEBUG: regenerateMessage called with content: $userMessageContent',
);
final reviewerMode = ref.read(reviewerModeProvider);
final api = ref.read(apiServiceProvider);
final selectedModel = ref.read(selectedModelProvider);
@@ -416,14 +424,14 @@ Future<void> regenerateMessage(
final responseText = ReviewerModeService.generateResponse(
userMessage: userMessageContent,
);
// Simulate streaming response
final words = responseText.split(' ');
for (final word in words) {
await Future.delayed(const Duration(milliseconds: 40));
ref.read(chatMessagesProvider.notifier).appendToLastMessage('$word ');
}
ref.read(chatMessagesProvider.notifier).finishStreaming();
await _saveConversationLocally(ref);
return;
@@ -433,14 +441,15 @@ Future<void> regenerateMessage(
try {
// Get conversation history for context (excluding the removed assistant message)
final List<ChatMessage> messages = ref.read(chatMessagesProvider);
final List<Map<String, dynamic>> conversationMessages = <Map<String, dynamic>>[];
final List<Map<String, dynamic>> conversationMessages =
<Map<String, dynamic>>[];
for (final msg in messages) {
if (msg.role.isNotEmpty && msg.content.isNotEmpty && !msg.isStreaming) {
// Handle messages with attachments
if (msg.attachmentIds != null && msg.attachmentIds!.isNotEmpty) {
final List<Map<String, dynamic>> contentArray = [];
// Add text content first
if (msg.content.isNotEmpty) {
contentArray.add({'type': 'text', 'text': msg.content});
@@ -452,10 +461,7 @@ Future<void> regenerateMessage(
});
} else {
// Regular text message
conversationMessages.add({
'role': msg.role,
'content': msg.content,
});
conversationMessages.add({'role': msg.role, 'content': msg.content});
}
}
}
@@ -496,7 +502,6 @@ Future<void> regenerateMessage(
ref.read(chatMessagesProvider.notifier).finishStreaming();
await _saveConversationLocally(ref);
} catch (e) {
debugPrint('DEBUG: Error during message regeneration: $e');
rethrow;
@@ -541,11 +546,15 @@ Future<void> _sendMessageInternal(
// Check if we need to create a new conversation first
var activeConversation = ref.read(activeConversationProvider);
debugPrint('DEBUG: Active conversation before send: ${activeConversation?.id}');
debugPrint(
'DEBUG: Active conversation before send: ${activeConversation?.id}',
);
// Create user message first
debugPrint('DEBUG: Creating user message with attachments: $attachments, tools: $toolIds');
debugPrint(
'DEBUG: Creating user message with attachments: $attachments, tools: $toolIds',
);
final userMessage = ChatMessage(
id: const Uuid().v4(),
role: 'user',
@@ -581,25 +590,25 @@ Future<void> _sendMessageInternal(
);
final updatedConversation = localConversation.copyWith(
id: serverConversation.id,
messages: serverConversation.messages.isNotEmpty
? serverConversation.messages
messages: serverConversation.messages.isNotEmpty
? serverConversation.messages
: [userMessage],
);
ref.read(activeConversationProvider.notifier).state =
updatedConversation;
activeConversation = updatedConversation;
// Set messages in the messages provider to keep UI in sync
ref.read(chatMessagesProvider.notifier).clearMessages();
ref.read(chatMessagesProvider.notifier).addMessage(userMessage);
debugPrint(
'DEBUG: Created conversation ${serverConversation.id} on server with first message',
);
debugPrint(
'DEBUG: Server conversation ID: ${serverConversation.id}, Title: ${serverConversation.title}',
);
// Invalidate conversations provider to refresh the list
// Adding a small delay to prevent rapid invalidations that could cause duplicates
Future.delayed(const Duration(milliseconds: 100), () {
@@ -790,14 +799,18 @@ Future<void> _sendMessageInternal(
}
}
// Check if web search is enabled for API
// Check feature toggles for API
final webSearchEnabled = ref.read(webSearchEnabledProvider);
// Debug log to track web search state
final imageGenerationEnabled = ref.read(imageGenerationEnabledProvider);
// Debug log to track feature toggle states
debugPrint('DEBUG: Web search toggle state: $webSearchEnabled');
debugPrint('DEBUG: Image generation toggle state: $imageGenerationEnabled');
// Prepare tools list - pass tool IDs directly
final List<String>? toolIdsForApi = (toolIds != null && toolIds.isNotEmpty) ? toolIds : null;
final List<String>? toolIdsForApi = (toolIds != null && toolIds.isNotEmpty)
? toolIds
: null;
if (toolIdsForApi != null) {
debugPrint('DEBUG: Including tool IDs: $toolIdsForApi');
}
@@ -934,6 +947,7 @@ Future<void> _sendMessageInternal(
conversationId: activeConversation?.id,
toolIds: toolIdsForApi,
enableWebSearch: webSearchEnabled,
enableImageGeneration: imageGenerationEnabled,
modelItem: modelItem,
);
@@ -971,7 +985,7 @@ Future<void> _sendMessageInternal(
// Create a stream controller for persistent handling
final persistentController = StreamController<String>.broadcast();
// Register stream with persistent service for app lifecycle handling
final persistentService = PersistentStreamingService();
final streamId = persistentService.registerStream(
@@ -1001,43 +1015,48 @@ Future<void> _sendMessageInternal(
// Track web search status
bool isSearching = false;
final streamSubscription = persistentController.stream.listen(
(chunk) {
debugPrint('DEBUG: Received stream chunk: "$chunk"');
// Check for web search indicators in the stream
if (webSearchEnabled && !isSearching) {
// Check if this is the start of web search
if (chunk.contains('[SEARCHING]') ||
chunk.contains('Searching the web') ||
if (chunk.contains('[SEARCHING]') ||
chunk.contains('Searching the web') ||
chunk.contains('web search')) {
isSearching = true;
// Update the message to show search status
ref.read(chatMessagesProvider.notifier).updateLastMessageWithFunction(
(message) => message.copyWith(
content: '🔍 Searching the web...',
metadata: {'webSearchActive': true},
),
);
ref
.read(chatMessagesProvider.notifier)
.updateLastMessageWithFunction(
(message) => message.copyWith(
content: '🔍 Searching the web...',
metadata: {'webSearchActive': true},
),
);
return; // Don't append this chunk
}
}
// Check if web search is complete
if (isSearching && (chunk.contains('[/SEARCHING]') ||
chunk.contains('Search complete'))) {
if (isSearching &&
(chunk.contains('[/SEARCHING]') ||
chunk.contains('Search complete'))) {
isSearching = false;
// Clear the search status message
ref.read(chatMessagesProvider.notifier).updateLastMessageWithFunction(
(message) => message.copyWith(
content: '',
metadata: {'webSearchActive': false},
),
);
ref
.read(chatMessagesProvider.notifier)
.updateLastMessageWithFunction(
(message) => message.copyWith(
content: '',
metadata: {'webSearchActive': false},
),
);
return; // Don't append this chunk
}
// Regular content - append to message
if (!chunk.contains('[SEARCHING]') && !chunk.contains('[/SEARCHING]')) {
ref.read(chatMessagesProvider.notifier).appendToLastMessage(chunk);
@@ -1071,7 +1090,7 @@ Future<void> _sendMessageInternal(
// For assistant messages, add completion details
if (msg.role == 'assistant') {
messageMap['model'] = selectedModel.id;
// Add mock usage data if not available (OpenWebUI expects this)
if (msg.usage != null) {
messageMap['usage'] = msg.usage;
@@ -1110,7 +1129,6 @@ Future<void> _sendMessageInternal(
debugPrint(
'DEBUG: Chat completed notification sent successfully for chat ID: ${activeConversation.id}',
);
} catch (e) {
debugPrint('DEBUG: Chat completed notification failed: $e');
debugPrint('DEBUG: Error details: $e');
@@ -1120,7 +1138,7 @@ Future<void> _sendMessageInternal(
// Fetch the latest conversation state without waiting for title generation
debugPrint('DEBUG: Fetching latest conversation state...');
debugPrint('DEBUG: Current message count: ${messages.length}');
try {
// Quick fetch to get the current state - no waiting for title generation
final updatedConv = await api.getConversation(
@@ -1133,11 +1151,18 @@ Future<void> _sendMessageInternal(
messages.length <= 2 &&
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);
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
@@ -1292,10 +1317,8 @@ Future<void> _sendMessageInternal(
}
// Streaming already marked as complete when stream ended
debugPrint(
'DEBUG: Server content replacement completed',
);
debugPrint('DEBUG: Server content replacement completed');
// Start background title check for first message exchanges
if (messages.length <= 2 && updatedConv.title == 'New Chat') {
debugPrint('DEBUG: Starting background title check...');
@@ -1320,6 +1343,127 @@ Future<void> _sendMessageInternal(
await Future.delayed(const Duration(milliseconds: 100));
await _saveConversationToServer(ref);
debugPrint('DEBUG: Conversation save completed');
// If image generation is enabled, trigger image generation with the user's prompt
if (imageGenerationEnabled) {
try {
debugPrint('DEBUG: Image generation enabled - triggering request');
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) {
debugPrint(
'DEBUG: Image generation returned ${generatedFiles.length} file(s)',
);
// 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);
} else {
debugPrint('DEBUG: No images found in generation response');
}
} catch (e) {
debugPrint('DEBUG: Image generation failed: $e');
}
}
},
onError: (error) {
debugPrint('DEBUG: Stream error in chat provider: $error');
@@ -1354,8 +1498,7 @@ Future<void> _sendMessageInternal(
final errorMessage = ChatMessage(
id: const Uuid().v4(),
role: 'assistant',
content:
'''⚠️ **Message Format Error**
content: '''⚠️ **Message Format Error**
This might be because:
• Image attachment couldn't be processed
@@ -1382,8 +1525,7 @@ This might be because:
final errorMessage = ChatMessage(
id: const Uuid().v4(),
role: 'assistant',
content:
'''⚠️ **Server Error**
content: '''⚠️ **Server Error**
This usually means:
• OpenWebUI server is experiencing issues
@@ -1407,8 +1549,7 @@ This usually means:
final errorMessage = ChatMessage(
id: const Uuid().v4(),
role: 'assistant',
content:
'''⏱️ **Request Timeout**
content: '''⏱️ **Request Timeout**
This might be because:
• Server taking too long to respond
@@ -1511,19 +1652,23 @@ Future<void> _triggerTitleGeneration(
try {
final api = ref.read(apiServiceProvider);
if (api == null) return;
debugPrint('DEBUG: Requesting title generation for conversation $conversationId');
debugPrint(
'DEBUG: Requesting title generation for conversation $conversationId',
);
// Call the title generation endpoint
final generatedTitle = await api.generateTitle(
conversationId: conversationId,
messages: messages,
model: model,
);
if (generatedTitle != null && generatedTitle.isNotEmpty && generatedTitle != 'New Chat') {
if (generatedTitle != null &&
generatedTitle.isNotEmpty &&
generatedTitle != 'New Chat') {
debugPrint('DEBUG: Title generated successfully: $generatedTitle');
// Update the active conversation with the new title
final activeConversation = ref.read(activeConversationProvider);
if (activeConversation?.id == conversationId) {
@@ -1532,10 +1677,12 @@ Future<void> _triggerTitleGeneration(
updatedAt: DateTime.now(),
);
ref.read(activeConversationProvider.notifier).state = updated;
// Save the updated title to the server
try {
debugPrint('DEBUG: Saving generated title to server: $generatedTitle');
debugPrint(
'DEBUG: Saving generated title to server: $generatedTitle',
);
final currentMessages = ref.read(chatMessagesProvider);
await api.updateConversationWithMessages(
conversationId,
@@ -1547,7 +1694,7 @@ Future<void> _triggerTitleGeneration(
} catch (e) {
debugPrint('DEBUG: Failed to save title to server: $e');
}
// Refresh the conversations list
ref.invalidate(conversationsProvider);
}
@@ -1564,22 +1711,27 @@ Future<void> _triggerTitleGeneration(
}
// Background function to check for title updates without blocking UI
Future<void> _checkForTitleInBackground(dynamic ref, String conversationId) async {
Future<void> _checkForTitleInBackground(
dynamic ref,
String conversationId,
) async {
try {
final api = ref.read(apiServiceProvider);
if (api == null) return;
// Wait a bit before first check to give server time to generate
await Future.delayed(const Duration(seconds: 3));
// Try a few times with increasing delays
for (int i = 0; i < 3; i++) {
try {
final updatedConv = await api.getConversation(conversationId);
if (updatedConv.title != 'New Chat' && updatedConv.title.isNotEmpty) {
debugPrint('DEBUG: Background title update found: ${updatedConv.title}');
debugPrint(
'DEBUG: Background title update found: ${updatedConv.title}',
);
// Update the active conversation with the new title
final activeConversation = ref.read(activeConversationProvider);
if (activeConversation?.id == conversationId) {
@@ -1588,14 +1740,14 @@ Future<void> _checkForTitleInBackground(dynamic ref, String conversationId) asyn
updatedAt: DateTime.now(),
);
ref.read(activeConversationProvider.notifier).state = updated;
// Refresh the conversations list
ref.invalidate(conversationsProvider);
}
return; // Title found, stop checking
}
// Wait before next check (3s, 5s, 7s)
if (i < 2) {
await Future.delayed(Duration(seconds: 2 + (i * 2)));
@@ -1605,8 +1757,10 @@ Future<void> _checkForTitleInBackground(dynamic ref, String conversationId) asyn
break; // Stop on error
}
}
debugPrint('DEBUG: Background title check completed without finding generated title');
debugPrint(
'DEBUG: Background title check completed without finding generated title',
);
} catch (e) {
debugPrint('DEBUG: Background title check failed: $e');
}
@@ -1646,9 +1800,7 @@ Future<void> _saveConversationToServer(dynamic ref) async {
debugPrint(
'DEBUG: Conversation ID being updated: ${activeConversation.id}',
);
debugPrint(
'DEBUG: Number of messages to save: ${messages.length}',
);
debugPrint('DEBUG: Number of messages to save: ${messages.length}');
try {
await api.updateConversationWithMessages(
@@ -1722,15 +1874,17 @@ Future<void> _saveConversationLocally(dynamic ref) async {
// Store conversation locally using the storage service's actual methods
final conversationsJson = await storage.getString('conversations') ?? '[]';
final List<dynamic> conversations = jsonDecode(conversationsJson);
// Find and update or add the conversation
final existingIndex = conversations.indexWhere((c) => c['id'] == updatedConversation.id);
final existingIndex = conversations.indexWhere(
(c) => c['id'] == updatedConversation.id,
);
if (existingIndex >= 0) {
conversations[existingIndex] = updatedConversation.toJson();
} else {
conversations.add(updatedConversation.toJson());
}
await storage.setString('conversations', jsonEncode(conversations));
ref.read(activeConversationProvider.notifier).state = updatedConversation;
ref.invalidate(conversationsProvider);

View File

@@ -6,6 +6,7 @@ import 'dart:io' show Platform;
import '../../../core/models/tool.dart';
import '../../../shared/theme/theme_extensions.dart';
import '../../chat/providers/chat_providers.dart';
import '../../../core/providers/app_providers.dart';
import '../providers/tools_providers.dart';
class UnifiedToolsModal extends ConsumerStatefulWidget {
@@ -19,6 +20,8 @@ class _UnifiedToolsModalState extends ConsumerState<UnifiedToolsModal> {
@override
Widget build(BuildContext context) {
final webSearchEnabled = ref.watch(webSearchEnabledProvider);
final imageGenEnabled = ref.watch(imageGenerationEnabledProvider);
final imageGenAvailable = ref.watch(imageGenerationAvailableProvider);
final selectedToolIds = ref.watch(selectedToolIdsProvider);
final toolsAsync = ref.watch(toolsListProvider);
@@ -60,6 +63,12 @@ class _UnifiedToolsModalState extends ConsumerState<UnifiedToolsModal> {
_buildWebSearchToggle(webSearchEnabled),
const SizedBox(height: Spacing.md),
// Image Generation Toggle (conditionally shown)
if (imageGenAvailable) ...[
_buildImageGenerationToggle(imageGenEnabled),
const SizedBox(height: Spacing.md),
],
// Tools Section
toolsAsync.when(
data: (tools) {
@@ -95,10 +104,15 @@ class _UnifiedToolsModalState extends ConsumerState<UnifiedToolsModal> {
),
),
const SizedBox(height: Spacing.sm),
...tools.map((tool) => Padding(
padding: const EdgeInsets.only(bottom: Spacing.sm),
child: _buildToolCard(tool, selectedToolIds.contains(tool.id)),
)),
...tools.map(
(tool) => Padding(
padding: const EdgeInsets.only(bottom: Spacing.sm),
child: _buildToolCard(
tool,
selectedToolIds.contains(tool.id),
),
),
),
],
);
},
@@ -221,17 +235,95 @@ class _UnifiedToolsModalState extends ConsumerState<UnifiedToolsModal> {
);
}
Widget _buildImageGenerationToggle(bool imageGenEnabled) {
return GestureDetector(
onTap: () {
HapticFeedback.lightImpact();
ref.read(imageGenerationEnabledProvider.notifier).state =
!imageGenEnabled;
},
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(Spacing.md),
decoration: BoxDecoration(
color: imageGenEnabled
? context.conduitTheme.buttonPrimary
: context.conduitTheme.cardBackground,
borderRadius: BorderRadius.circular(AppBorderRadius.md),
border: Border.all(
color: imageGenEnabled
? context.conduitTheme.buttonPrimary
: context.conduitTheme.cardBorder,
width: BorderWidth.regular,
),
),
child: Row(
children: [
Icon(
Platform.isIOS ? CupertinoIcons.photo : Icons.image,
size: IconSize.medium,
color: imageGenEnabled
? context.conduitTheme.buttonPrimaryText
: context.conduitTheme.textPrimary.withValues(
alpha: Alpha.strong,
),
),
const SizedBox(width: Spacing.sm),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Image Generation',
style: AppTypography.labelStyle.copyWith(
color: imageGenEnabled
? context.conduitTheme.buttonPrimaryText
: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w600,
),
),
Text(
imageGenEnabled
? 'I can generate images from your prompt'
: 'Enable to generate images with your request',
style: AppTypography.captionStyle.copyWith(
color: imageGenEnabled
? context.conduitTheme.buttonPrimaryText.withValues(
alpha: Alpha.strong,
)
: context.conduitTheme.textSecondary,
),
),
],
),
),
Icon(
imageGenEnabled ? Icons.toggle_on : Icons.toggle_off,
size: IconSize.large,
color: imageGenEnabled
? context.conduitTheme.buttonPrimaryText
: context.conduitTheme.textSecondary,
),
],
),
),
);
}
Widget _buildToolCard(Tool tool, bool isSelected) {
return GestureDetector(
onTap: () {
HapticFeedback.lightImpact();
final currentIds = ref.read(selectedToolIdsProvider);
if (isSelected) {
ref.read(selectedToolIdsProvider.notifier).state =
currentIds.where((id) => id != tool.id).toList();
ref.read(selectedToolIdsProvider.notifier).state = currentIds
.where((id) => id != tool.id)
.toList();
} else {
ref.read(selectedToolIdsProvider.notifier).state =
[...currentIds, tool.id];
ref.read(selectedToolIdsProvider.notifier).state = [
...currentIds,
tool.id,
];
}
},
child: Container(
@@ -274,7 +366,7 @@ class _UnifiedToolsModalState extends ConsumerState<UnifiedToolsModal> {
fontWeight: FontWeight.w600,
),
),
if (tool.meta?['description'] != null &&
if (tool.meta?['description'] != null &&
tool.meta!['description'].toString().isNotEmpty)
Text(
tool.meta!['description'].toString(),
@@ -306,11 +398,13 @@ class _UnifiedToolsModalState extends ConsumerState<UnifiedToolsModal> {
IconData _getToolIcon(Tool tool) {
final toolName = tool.name.toLowerCase();
if (toolName.contains('image') || toolName.contains('vision')) {
return Platform.isIOS ? CupertinoIcons.photo : Icons.image;
} else if (toolName.contains('code') || toolName.contains('python')) {
return Platform.isIOS ? CupertinoIcons.chevron_left_slash_chevron_right : Icons.code;
return Platform.isIOS
? CupertinoIcons.chevron_left_slash_chevron_right
: Icons.code;
} else if (toolName.contains('calculator') || toolName.contains('math')) {
return Icons.calculate;
} else if (toolName.contains('file') || toolName.contains('document')) {