feat: tools implementation
This commit is contained in:
@@ -507,20 +507,22 @@ Future<void> regenerateMessage(
|
||||
Future<void> sendMessage(
|
||||
WidgetRef ref,
|
||||
String message,
|
||||
List<String>? attachments,
|
||||
) async {
|
||||
List<String>? attachments, [
|
||||
List<String>? toolIds,
|
||||
]) async {
|
||||
debugPrint(
|
||||
'DEBUG: sendMessage called with message: $message, attachments: $attachments',
|
||||
'DEBUG: sendMessage called with message: $message, attachments: $attachments, tools: $toolIds',
|
||||
);
|
||||
await _sendMessageInternal(ref, message, attachments);
|
||||
await _sendMessageInternal(ref, message, attachments, toolIds);
|
||||
}
|
||||
|
||||
// Internal send message implementation
|
||||
Future<void> _sendMessageInternal(
|
||||
dynamic ref,
|
||||
String message,
|
||||
List<String>? attachments,
|
||||
) async {
|
||||
List<String>? attachments, [
|
||||
List<String>? toolIds,
|
||||
]) async {
|
||||
debugPrint('DEBUG: _sendMessageInternal called');
|
||||
debugPrint('DEBUG: Message: $message');
|
||||
debugPrint('DEBUG: Attachments: $attachments');
|
||||
@@ -543,7 +545,7 @@ Future<void> _sendMessageInternal(
|
||||
debugPrint('DEBUG: Active conversation before send: ${activeConversation?.id}');
|
||||
|
||||
// Create user message first
|
||||
debugPrint('DEBUG: Creating user message with attachments: $attachments');
|
||||
debugPrint('DEBUG: Creating user message with attachments: $attachments, tools: $toolIds');
|
||||
final userMessage = ChatMessage(
|
||||
id: const Uuid().v4(),
|
||||
role: 'user',
|
||||
@@ -794,8 +796,11 @@ Future<void> _sendMessageInternal(
|
||||
// Debug log to track web search state
|
||||
debugPrint('DEBUG: Web search toggle state: $webSearchEnabled');
|
||||
|
||||
// No need for function calling tools since we're using retrieval directly
|
||||
final tools = <Map<String, dynamic>>[];
|
||||
// Prepare tools list - pass tool IDs directly
|
||||
final List<String>? toolIdsForApi = (toolIds != null && toolIds.isNotEmpty) ? toolIds : null;
|
||||
if (toolIdsForApi != null) {
|
||||
debugPrint('DEBUG: Including tool IDs: $toolIdsForApi');
|
||||
}
|
||||
|
||||
try {
|
||||
// Use the model's actual supported parameters if available
|
||||
@@ -927,7 +932,7 @@ Future<void> _sendMessageInternal(
|
||||
messages: conversationMessages,
|
||||
model: selectedModel.id,
|
||||
conversationId: activeConversation?.id,
|
||||
tools: tools.isNotEmpty ? tools : null,
|
||||
toolIds: toolIdsForApi,
|
||||
enableWebSearch: webSearchEnabled,
|
||||
modelItem: modelItem,
|
||||
);
|
||||
|
||||
@@ -22,6 +22,7 @@ import '../services/file_attachment_service.dart';
|
||||
import '../../navigation/views/chats_list_page.dart';
|
||||
import '../../files/views/files_page.dart';
|
||||
import '../../profile/views/profile_page.dart';
|
||||
import '../../tools/providers/tools_providers.dart';
|
||||
import '../../../shared/widgets/offline_indicator.dart';
|
||||
import '../../../core/services/connectivity_service.dart';
|
||||
import '../../../core/models/chat_message.dart';
|
||||
@@ -289,11 +290,16 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
|
||||
debugPrint('DEBUG: Uploaded file IDs: $uploadedFileIds');
|
||||
|
||||
// Send message with file attachments using existing provider logic
|
||||
// Get selected tools
|
||||
final toolIds = ref.read(selectedToolIdsProvider);
|
||||
debugPrint('DEBUG: Selected tool IDs: $toolIds');
|
||||
|
||||
// Send message with file attachments and tools using existing provider logic
|
||||
await sendMessage(
|
||||
ref,
|
||||
text,
|
||||
uploadedFileIds.isNotEmpty ? uploadedFileIds : null,
|
||||
toolIds.isNotEmpty ? toolIds : null,
|
||||
);
|
||||
|
||||
debugPrint('DEBUG: Message sent successfully');
|
||||
@@ -744,8 +750,6 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
).push(MaterialPageRoute(builder: (context) => const FilesPage()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
void _navigateToProfile() {
|
||||
Navigator.of(
|
||||
context,
|
||||
|
||||
@@ -8,6 +8,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'dart:io' show Platform;
|
||||
import 'dart:async';
|
||||
import '../providers/chat_providers.dart';
|
||||
import '../../tools/widgets/tool_selector.dart';
|
||||
import '../../tools/providers/tools_providers.dart';
|
||||
|
||||
import '../../../shared/utils/platform_utils.dart';
|
||||
|
||||
@@ -47,6 +49,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
late AnimationController _pulseController;
|
||||
Timer? _blurCollapseTimer;
|
||||
bool _hasAutoFocusedOnce = false;
|
||||
bool _showToolSelector = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -165,6 +168,11 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
PlatformUtils.lightHaptic();
|
||||
widget.onSendMessage(text);
|
||||
_controller.clear();
|
||||
setState(() {
|
||||
_showToolSelector = false;
|
||||
});
|
||||
// Clear selected tools after sending
|
||||
ref.read(selectedToolIdsProvider.notifier).state = [];
|
||||
// Keep input expanded and focused for better UX - don't dismiss keyboard
|
||||
// KeyboardUtils.dismissKeyboard(context);
|
||||
// _setExpanded(false);
|
||||
@@ -346,6 +354,14 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
|
||||
// Expanded bottom row with additional options
|
||||
if (_isExpanded) ...[
|
||||
// Tool selector
|
||||
if (_showToolSelector)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: Spacing.inputPadding,
|
||||
),
|
||||
child: ToolSelector(),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Spacing.inputPadding,
|
||||
@@ -364,6 +380,20 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
tooltip: 'Add attachment',
|
||||
),
|
||||
const SizedBox(width: Spacing.sm),
|
||||
// Tools button
|
||||
_buildRoundButton(
|
||||
icon: Icons.build,
|
||||
onTap: widget.enabled
|
||||
? () {
|
||||
setState(() {
|
||||
_showToolSelector = !_showToolSelector;
|
||||
});
|
||||
}
|
||||
: null,
|
||||
tooltip: 'Tools',
|
||||
isActive: _showToolSelector || ref.watch(selectedToolIdsProvider).isNotEmpty,
|
||||
),
|
||||
const SizedBox(width: Spacing.sm),
|
||||
Flexible(
|
||||
child: Center(child: _buildResearchToggle()),
|
||||
),
|
||||
|
||||
11
lib/features/tools/providers/tools_providers.dart
Normal file
11
lib/features/tools/providers/tools_providers.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:conduit/core/models/tool.dart';
|
||||
import 'package:conduit/core/services/tools_service.dart';
|
||||
|
||||
final toolsListProvider = FutureProvider<List<Tool>>((ref) async {
|
||||
final toolsService = ref.watch(toolsServiceProvider);
|
||||
if (toolsService == null) return [];
|
||||
return await toolsService.getTools();
|
||||
});
|
||||
|
||||
final selectedToolIdsProvider = StateProvider<List<String>>((ref) => []);
|
||||
61
lib/features/tools/widgets/tool_selector.dart
Normal file
61
lib/features/tools/widgets/tool_selector.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:conduit/features/tools/providers/tools_providers.dart';
|
||||
|
||||
class ToolSelector extends ConsumerWidget {
|
||||
const ToolSelector({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final toolsAsync = ref.watch(toolsListProvider);
|
||||
final selectedIds = ref.watch(selectedToolIdsProvider);
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return toolsAsync.when(
|
||||
data: (tools) {
|
||||
if (tools.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
height: 40,
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
itemCount: tools.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(width: 8),
|
||||
itemBuilder: (context, index) {
|
||||
final tool = tools[index];
|
||||
final isSelected = selectedIds.contains(tool.id);
|
||||
|
||||
return FilterChip(
|
||||
label: Text(tool.name),
|
||||
selected: isSelected,
|
||||
onSelected: (_) {
|
||||
final currentIds = ref.read(selectedToolIdsProvider);
|
||||
if (isSelected) {
|
||||
ref.read(selectedToolIdsProvider.notifier).state =
|
||||
currentIds.where((id) => id != tool.id).toList();
|
||||
} else {
|
||||
ref.read(selectedToolIdsProvider.notifier).state =
|
||||
[...currentIds, tool.id];
|
||||
}
|
||||
},
|
||||
avatar: Icon(
|
||||
Icons.build,
|
||||
size: 16,
|
||||
color: isSelected
|
||||
? theme.colorScheme.onSecondaryContainer
|
||||
: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => const SizedBox.shrink(),
|
||||
error: (error, stack) => const SizedBox.shrink(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user