Files
iiEsaywebUIapp/lib/features/tools/widgets/unified_tools_modal.dart
2025-08-21 13:02:50 +05:30

329 lines
12 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:io' show Platform;
import '../../../core/models/tool.dart';
import '../../../shared/theme/theme_extensions.dart';
import '../../chat/providers/chat_providers.dart';
import '../providers/tools_providers.dart';
class UnifiedToolsModal extends ConsumerStatefulWidget {
const UnifiedToolsModal({super.key});
@override
ConsumerState<UnifiedToolsModal> createState() => _UnifiedToolsModalState();
}
class _UnifiedToolsModalState extends ConsumerState<UnifiedToolsModal> {
@override
Widget build(BuildContext context) {
final webSearchEnabled = ref.watch(webSearchEnabledProvider);
final selectedToolIds = ref.watch(selectedToolIdsProvider);
final toolsAsync = ref.watch(toolsListProvider);
return Container(
decoration: BoxDecoration(
color: context.conduitTheme.surfaceBackground,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(AppBorderRadius.bottomSheet),
),
boxShadow: ConduitShadows.modal,
),
padding: const EdgeInsets.all(Spacing.bottomSheetPadding),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Handle bar
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: context.conduitTheme.textPrimary.withValues(
alpha: Alpha.medium,
),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: Spacing.lg),
// Title
Text(
'Tools & Search',
style: AppTypography.headlineSmallStyle.copyWith(
color: context.conduitTheme.textPrimary,
),
),
const SizedBox(height: Spacing.lg),
// Web Search Toggle
_buildWebSearchToggle(webSearchEnabled),
const SizedBox(height: Spacing.md),
// Tools Section
toolsAsync.when(
data: (tools) {
if (tools.isEmpty) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(Spacing.md),
decoration: BoxDecoration(
color: context.conduitTheme.cardBackground,
borderRadius: BorderRadius.circular(AppBorderRadius.md),
border: Border.all(
color: context.conduitTheme.cardBorder,
width: BorderWidth.regular,
),
),
child: Text(
'No tools available',
style: AppTypography.bodySmallStyle.copyWith(
color: context.conduitTheme.textSecondary,
),
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Available Tools',
style: AppTypography.labelStyle.copyWith(
color: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: Spacing.sm),
...tools.map((tool) => Padding(
padding: const EdgeInsets.only(bottom: Spacing.sm),
child: _buildToolCard(tool, selectedToolIds.contains(tool.id)),
)),
],
);
},
loading: () => Container(
width: double.infinity,
padding: const EdgeInsets.all(Spacing.md),
decoration: BoxDecoration(
color: context.conduitTheme.cardBackground,
borderRadius: BorderRadius.circular(AppBorderRadius.md),
border: Border.all(
color: context.conduitTheme.cardBorder,
width: BorderWidth.regular,
),
),
child: const Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
),
error: (error, stack) => Container(
width: double.infinity,
padding: const EdgeInsets.all(Spacing.md),
decoration: BoxDecoration(
color: context.conduitTheme.cardBackground,
borderRadius: BorderRadius.circular(AppBorderRadius.md),
border: Border.all(
color: context.conduitTheme.cardBorder,
width: BorderWidth.regular,
),
),
child: Text(
'Failed to load tools',
style: AppTypography.bodySmallStyle.copyWith(
color: context.conduitTheme.error,
),
),
),
),
],
),
);
}
Widget _buildWebSearchToggle(bool webSearchEnabled) {
return GestureDetector(
onTap: () {
HapticFeedback.lightImpact();
ref.read(webSearchEnabledProvider.notifier).state = !webSearchEnabled;
},
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(Spacing.md),
decoration: BoxDecoration(
color: webSearchEnabled
? context.conduitTheme.buttonPrimary
: context.conduitTheme.cardBackground,
borderRadius: BorderRadius.circular(AppBorderRadius.md),
border: Border.all(
color: webSearchEnabled
? context.conduitTheme.buttonPrimary
: context.conduitTheme.cardBorder,
width: BorderWidth.regular,
),
),
child: Row(
children: [
Icon(
webSearchEnabled
? (Platform.isIOS ? CupertinoIcons.globe : Icons.public)
: (Platform.isIOS ? CupertinoIcons.search : Icons.search),
size: IconSize.medium,
color: webSearchEnabled
? context.conduitTheme.buttonPrimaryText
: context.conduitTheme.textPrimary.withValues(
alpha: Alpha.strong,
),
),
const SizedBox(width: Spacing.sm),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Web Search',
style: AppTypography.labelStyle.copyWith(
color: webSearchEnabled
? context.conduitTheme.buttonPrimaryText
: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w600,
),
),
Text(
webSearchEnabled
? 'I can search the internet for information'
: 'Enable to search the web for answers',
style: AppTypography.captionStyle.copyWith(
color: webSearchEnabled
? context.conduitTheme.buttonPrimaryText.withValues(
alpha: Alpha.strong,
)
: context.conduitTheme.textSecondary,
),
),
],
),
),
Icon(
webSearchEnabled ? Icons.toggle_on : Icons.toggle_off,
size: IconSize.large,
color: webSearchEnabled
? 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();
} else {
ref.read(selectedToolIdsProvider.notifier).state =
[...currentIds, tool.id];
}
},
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(Spacing.md),
decoration: BoxDecoration(
color: isSelected
? context.conduitTheme.buttonPrimary
: context.conduitTheme.cardBackground,
borderRadius: BorderRadius.circular(AppBorderRadius.md),
border: Border.all(
color: isSelected
? context.conduitTheme.buttonPrimary
: context.conduitTheme.cardBorder,
width: BorderWidth.regular,
),
),
child: Row(
children: [
Icon(
_getToolIcon(tool),
size: IconSize.medium,
color: isSelected
? context.conduitTheme.buttonPrimaryText
: context.conduitTheme.textPrimary.withValues(
alpha: Alpha.strong,
),
),
const SizedBox(width: Spacing.sm),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
tool.name,
style: AppTypography.labelStyle.copyWith(
color: isSelected
? context.conduitTheme.buttonPrimaryText
: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w600,
),
),
if (tool.meta?['description'] != null &&
tool.meta!['description'].toString().isNotEmpty)
Text(
tool.meta!['description'].toString(),
style: AppTypography.captionStyle.copyWith(
color: isSelected
? context.conduitTheme.buttonPrimaryText.withValues(
alpha: Alpha.strong,
)
: context.conduitTheme.textSecondary,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
Icon(
isSelected ? Icons.toggle_on : Icons.toggle_off,
size: IconSize.large,
color: isSelected
? context.conduitTheme.buttonPrimaryText
: context.conduitTheme.textSecondary,
),
],
),
),
);
}
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;
} else if (toolName.contains('calculator') || toolName.contains('math')) {
return Icons.calculate;
} else if (toolName.contains('file') || toolName.contains('document')) {
return Platform.isIOS ? CupertinoIcons.doc : Icons.description;
} else if (toolName.contains('api') || toolName.contains('request')) {
return Platform.isIOS ? CupertinoIcons.link : Icons.api;
} else if (toolName.contains('chart') || toolName.contains('graph')) {
return Icons.bar_chart;
} else if (toolName.contains('data') || toolName.contains('database')) {
return Icons.storage;
} else {
return Icons.build;
}
}
}