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

@@ -896,6 +896,37 @@ final conversationSuggestionsProvider = FutureProvider<List<String>>((
}
});
// Server features and permissions
final userPermissionsProvider = FutureProvider<Map<String, dynamic>>((
ref,
) async {
final api = ref.watch(apiServiceProvider);
if (api == null) return {};
try {
return await api.getUserPermissions();
} catch (e) {
foundation.debugPrint('DEBUG: Error fetching user permissions: $e');
return {};
}
});
final imageGenerationAvailableProvider = Provider<bool>((ref) {
final perms = ref.watch(userPermissionsProvider);
return perms.maybeWhen(
data: (data) {
final features = data['features'];
if (features is Map<String, dynamic>) {
final value = features['image_generation'];
if (value is bool) return value;
if (value is String) return value.toLowerCase() == 'true';
}
return false;
},
orElse: () => false,
);
});
// Folders provider
final foldersProvider = FutureProvider<List<Folder>>((ref) async {
final api = ref.watch(apiServiceProvider);

View File

@@ -739,10 +739,7 @@ class ApiService {
userAttachments.add(file['file_id'] as String);
} else if (file['type'] == 'image' && file['url'] != null) {
// Generated image
generatedFiles.add({
'type': file['type'],
'url': file['url'],
});
generatedFiles.add({'type': file['type'], 'url': file['url']});
}
}
}
@@ -1688,9 +1685,8 @@ class ApiService {
required String text,
String? voice,
}) async {
debugPrint(
'DEBUG: Generating speech for text: ${text.substring(0, 50)}...',
);
final textPreview = text.length > 50 ? text.substring(0, 50) : text;
debugPrint('DEBUG: Generating speech for text: $textPreview...');
final response = await _dio.post(
'/api/v1/audio/speech',
data: {'text': text, if (voice != null) 'voice': voice},
@@ -1805,7 +1801,7 @@ class ApiService {
return [];
}
Future<Map<String, dynamic>> generateImage({
Future<dynamic> generateImage({
required String prompt,
String? model,
int? width,
@@ -1813,9 +1809,9 @@ class ApiService {
int? steps,
double? guidance,
}) async {
debugPrint(
'DEBUG: Generating image with prompt: ${prompt.substring(0, 50)}...',
);
final promptPreview = prompt.length > 50 ? prompt.substring(0, 50) : prompt;
debugPrint('DEBUG: Generating image with prompt: $promptPreview...');
try {
final response = await _dio.post(
'/api/v1/images/generations',
data: {
@@ -1827,7 +1823,23 @@ class ApiService {
if (guidance != null) 'guidance': guidance,
},
);
return response.data as Map<String, dynamic>;
return response.data;
} on DioException catch (e) {
debugPrint('DEBUG: images/generations failed: ${e.response?.statusCode}');
// Fallback to singular path some servers use
final response = await _dio.post(
'/api/v1/image/generations',
data: {
'prompt': prompt,
if (model != null) 'model': model,
if (width != null) 'width': width,
if (height != null) 'height': height,
if (steps != null) 'steps': steps,
if (guidance != null) 'guidance': guidance,
},
);
return response.data;
}
}
// Prompts
@@ -1841,6 +1853,24 @@ class ApiService {
return [];
}
// Permissions & Features
Future<Map<String, dynamic>> getUserPermissions() async {
debugPrint('DEBUG: Fetching user permissions');
try {
final response = await _dio.get('/api/v1/users/permissions');
return response.data as Map<String, dynamic>;
} catch (e) {
debugPrint('DEBUG: Error fetching user permissions: $e');
if (e is DioException) {
debugPrint('DEBUG: Permissions error response: ${e.response?.data}');
debugPrint(
'DEBUG: Permissions error status: ${e.response?.statusCode}',
);
}
rethrow;
}
}
Future<Map<String, dynamic>> createPrompt({
required String title,
required String content,
@@ -2434,6 +2464,7 @@ class ApiService {
String? conversationId,
List<String>? toolIds,
bool enableWebSearch = false,
bool enableImageGeneration = false,
Map<String, dynamic>? modelItem,
}) {
final streamController = StreamController<String>();
@@ -2504,17 +2535,25 @@ class ApiService {
data['chat_id'] = conversationId;
}
// Add web search flag if enabled
// Add feature flags if enabled
if (enableWebSearch) {
data['web_search'] = true;
// Also add it in features for compatibility
debugPrint('DEBUG: Web search enabled in SSE request');
}
if (enableImageGeneration) {
// Mirror web_search behavior for image generation
data['image_generation'] = true;
debugPrint('DEBUG: Image generation enabled in SSE request');
}
if (enableWebSearch || enableImageGeneration) {
// Include features map for compatibility
data['features'] = {
'web_search': true,
'image_generation': false,
'web_search': enableWebSearch,
'image_generation': enableImageGeneration,
'code_interpreter': false,
'memory': false,
};
debugPrint('DEBUG: Web search enabled in SSE request');
}
// Add tool_ids if provided (Open-WebUI expects tool_ids as array of strings)

View File

@@ -76,7 +76,9 @@ 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) {
@@ -100,14 +102,16 @@ class ChatMessagesNotifier extends StateNotifier<List<ChatMessage>> {
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
@@ -164,16 +168,15 @@ class ChatMessagesNotifier extends StateNotifier<List<ChatMessage>> {
];
}
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,7 +389,9 @@ 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);
@@ -433,7 +441,8 @@ 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) {
@@ -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;
@@ -542,10 +547,14 @@ 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',
@@ -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);
final imageGenerationEnabled = ref.read(imageGenerationEnabledProvider);
// Debug log to track web search state
// 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,
);
@@ -1014,7 +1028,9 @@ Future<void> _sendMessageInternal(
chunk.contains('web search')) {
isSearching = true;
// Update the message to show search status
ref.read(chatMessagesProvider.notifier).updateLastMessageWithFunction(
ref
.read(chatMessagesProvider.notifier)
.updateLastMessageWithFunction(
(message) => message.copyWith(
content: '🔍 Searching the web...',
metadata: {'webSearchActive': true},
@@ -1025,11 +1041,14 @@ Future<void> _sendMessageInternal(
}
// Check if web search is complete
if (isSearching && (chunk.contains('[/SEARCHING]') ||
if (isSearching &&
(chunk.contains('[/SEARCHING]') ||
chunk.contains('Search complete'))) {
isSearching = false;
// Clear the search status message
ref.read(chatMessagesProvider.notifier).updateLastMessageWithFunction(
ref
.read(chatMessagesProvider.notifier)
.updateLastMessageWithFunction(
(message) => message.copyWith(
content: '',
metadata: {'webSearchActive': false},
@@ -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');
@@ -1136,8 +1154,15 @@ Future<void> _sendMessageInternal(
// 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,9 +1317,7 @@ 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') {
@@ -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
@@ -1512,7 +1653,9 @@ Future<void> _triggerTitleGeneration(
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(
@@ -1521,7 +1664,9 @@ Future<void> _triggerTitleGeneration(
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
@@ -1535,7 +1680,9 @@ Future<void> _triggerTitleGeneration(
// 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,
@@ -1564,7 +1711,10 @@ 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;
@@ -1578,7 +1728,9 @@ Future<void> _checkForTitleInBackground(dynamic ref, String conversationId) asyn
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);
@@ -1606,7 +1758,9 @@ Future<void> _checkForTitleInBackground(dynamic ref, String conversationId) asyn
}
}
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(
@@ -1724,7 +1876,9 @@ Future<void> _saveConversationLocally(dynamic ref) async {
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 {

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(
...tools.map(
(tool) => Padding(
padding: const EdgeInsets.only(bottom: Spacing.sm),
child: _buildToolCard(tool, selectedToolIds.contains(tool.id)),
)),
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(
@@ -310,7 +402,9 @@ class _UnifiedToolsModalState extends ConsumerState<UnifiedToolsModal> {
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')) {