Files
iiEsaywebUIapp/lib/features/tools/widgets/unified_tools_modal.dart

458 lines
17 KiB
Dart
Raw Normal View History

2025-08-20 16:08:44 +05:30
import 'package:flutter/material.dart';
import 'package:conduit/l10n/app_localizations.dart';
2025-08-20 16:08:44 +05:30
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:io' show Platform;
2025-08-21 13:02:50 +05:30
import '../../../core/models/tool.dart';
2025-08-20 16:08:44 +05:30
import '../../../shared/theme/theme_extensions.dart';
import '../../chat/providers/chat_providers.dart';
2025-08-21 14:37:49 +05:30
import '../../../core/providers/app_providers.dart';
2025-08-20 16:08:44 +05:30
import '../providers/tools_providers.dart';
2025-08-22 01:24:04 +05:30
import '../../../shared/widgets/sheet_handle.dart';
2025-08-24 14:35:17 +05:30
import '../../chat/views/chat_page_helpers.dart';
2025-08-20 16:08:44 +05:30
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);
2025-08-21 14:37:49 +05:30
final imageGenEnabled = ref.watch(imageGenerationEnabledProvider);
final imageGenAvailable = ref.watch(imageGenerationAvailableProvider);
final webSearchAvailable = ref.watch(webSearchAvailableProvider);
2025-08-20 16:08:44 +05:30
final selectedToolIds = ref.watch(selectedToolIdsProvider);
final toolsAsync = ref.watch(toolsListProvider);
2025-08-21 23:56:47 +05:30
final theme = context.conduitTheme;
2025-08-20 16:08:44 +05:30
return Container(
decoration: BoxDecoration(
2025-08-21 23:56:47 +05:30
color: theme.surfaceBackground,
2025-08-20 16:08:44 +05:30
borderRadius: const BorderRadius.vertical(
top: Radius.circular(AppBorderRadius.bottomSheet),
),
2025-08-24 14:35:17 +05:30
border: Border.all(
color: theme.dividerColor,
width: BorderWidth.regular,
),
2025-08-20 16:08:44 +05:30
boxShadow: ConduitShadows.modal,
),
2025-08-21 23:56:47 +05:30
child: SafeArea(
top: false,
bottom: true,
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.8,
2025-08-20 16:08:44 +05:30
),
2025-08-21 23:56:47 +05:30
child: SingleChildScrollView(
padding: const EdgeInsets.all(Spacing.bottomSheetPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
2025-08-22 01:24:04 +05:30
const SheetHandle(),
2025-08-21 23:56:47 +05:30
const SizedBox(height: Spacing.md),
2025-08-20 16:08:44 +05:30
2025-08-24 14:35:17 +05:30
// Full tiles for Web and Image features
Column(
children: [
if (webSearchAvailable)
_buildFeatureTile(
title: AppLocalizations.of(context)!.webSearch,
description: AppLocalizations.of(
context,
)!.webSearchDescription,
icon: Platform.isIOS
? CupertinoIcons.search
: Icons.search,
isActive: webSearchEnabled,
onTap: () {
HapticFeedback.lightImpact();
ref.read(webSearchEnabledProvider.notifier).state =
!webSearchEnabled;
},
),
2025-08-24 14:35:17 +05:30
if (imageGenAvailable)
_buildFeatureTile(
title: AppLocalizations.of(context)!.imageGeneration,
description: AppLocalizations.of(
context,
)!.imageGenerationDescription,
2025-08-24 14:35:17 +05:30
icon: Platform.isIOS
? CupertinoIcons.photo
: Icons.image,
isActive: imageGenEnabled,
onTap: () {
HapticFeedback.lightImpact();
ref
.read(imageGenerationEnabledProvider.notifier)
.state =
!imageGenEnabled;
},
),
],
),
const SizedBox(height: Spacing.lg),
2025-08-21 14:37:49 +05:30
2025-08-24 14:35:17 +05:30
// All tools as selectable tiles (model selector style)
2025-08-21 23:56:47 +05:30
toolsAsync.when(
data: (tools) {
if (tools.isEmpty) {
return _buildNeutralCard(
child: Text(
'No tools available',
style: AppTypography.bodySmallStyle.copyWith(
color: theme.textSecondary,
),
),
);
}
2025-08-21 13:02:50 +05:30
2025-08-21 23:56:47 +05:30
return Column(
2025-08-24 14:35:17 +05:30
children: tools.map((tool) {
final isSelected = selectedToolIds.contains(tool.id);
return _buildToolTile(
tool,
isSelected,
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];
}
},
);
}).toList(),
2025-08-21 23:56:47 +05:30
);
},
loading: () => _buildNeutralCard(
child: const Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
),
2025-08-20 16:08:44 +05:30
),
),
2025-08-21 23:56:47 +05:30
error: (error, stack) => _buildNeutralCard(
child: Text(
'Failed to load tools',
style: AppTypography.bodySmallStyle.copyWith(
color: theme.error,
2025-08-21 14:37:49 +05:30
),
),
),
2025-08-21 13:02:50 +05:30
),
2025-08-21 23:56:47 +05:30
],
2025-08-20 16:08:44 +05:30
),
),
2025-08-21 23:56:47 +05:30
),
),
);
}
Widget _buildNeutralCard({required Widget child}) {
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,
),
2025-08-20 16:08:44 +05:30
),
2025-08-21 23:56:47 +05:30
child: child,
);
}
2025-08-24 14:35:17 +05:30
// Legacy header removed in simplified design
// Removed legacy builders (kept earlier for reference)
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;
}
2025-08-20 16:08:44 +05:30
}
2025-08-24 14:35:17 +05:30
Widget _buildFeatureTile({
required String title,
required String description,
required IconData icon,
required bool isActive,
required VoidCallback onTap,
}) {
return PressableScale(
onTap: onTap,
borderRadius: BorderRadius.circular(AppBorderRadius.md),
child: Container(
margin: const EdgeInsets.only(bottom: Spacing.md),
decoration: BoxDecoration(
gradient: isActive
? LinearGradient(
colors: [
context.conduitTheme.buttonPrimary.withValues(alpha: 0.2),
context.conduitTheme.buttonPrimary.withValues(alpha: 0.1),
],
)
: null,
color: isActive
? null
: context.conduitTheme.surfaceBackground.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(AppBorderRadius.md),
border: Border.all(
color: isActive
? context.conduitTheme.buttonPrimary.withValues(alpha: 0.5)
: context.conduitTheme.dividerColor,
width: BorderWidth.regular,
),
boxShadow: isActive ? ConduitShadows.card : null,
2025-08-21 23:56:47 +05:30
),
2025-08-24 14:35:17 +05:30
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: Spacing.md,
vertical: Spacing.sm,
2025-08-20 16:08:44 +05:30
),
2025-08-24 14:35:17 +05:30
child: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: context.conduitTheme.buttonPrimary.withValues(
alpha: 0.15,
2025-08-20 16:08:44 +05:30
),
2025-08-24 14:35:17 +05:30
borderRadius: BorderRadius.circular(AppBorderRadius.md),
),
child: Icon(
icon,
color: context.conduitTheme.buttonPrimary,
size: 16,
),
2025-08-20 16:08:44 +05:30
),
2025-08-24 14:35:17 +05:30
const SizedBox(width: Spacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
color: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w600,
fontSize: AppTypography.bodyMedium,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
2025-08-21 14:37:49 +05:30
),
2025-08-24 14:35:17 +05:30
const SizedBox(height: Spacing.xs),
Text(
description,
style: TextStyle(
color: context.conduitTheme.textSecondary,
fontSize: AppTypography.labelSmall,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
2025-08-21 14:37:49 +05:30
),
2025-08-24 14:35:17 +05:30
],
),
),
const SizedBox(width: Spacing.md),
AnimatedOpacity(
opacity: isActive ? 1 : 0.6,
duration: AnimationDuration.fast,
child: Container(
padding: const EdgeInsets.all(Spacing.xxs),
decoration: BoxDecoration(
color: isActive
? context.conduitTheme.buttonPrimary
: context.conduitTheme.surfaceBackground,
borderRadius: BorderRadius.circular(AppBorderRadius.md),
border: Border.all(
color: isActive
? context.conduitTheme.buttonPrimary.withValues(
alpha: 0.6,
2025-08-21 14:37:49 +05:30
)
2025-08-24 14:35:17 +05:30
: context.conduitTheme.dividerColor,
2025-08-21 14:37:49 +05:30
),
),
2025-08-24 14:35:17 +05:30
child: Icon(
isActive
? (Platform.isIOS
? CupertinoIcons.check_mark
: Icons.check)
: (Platform.isIOS ? CupertinoIcons.add : Icons.add),
color: isActive
? context.conduitTheme.textInverse
: context.conduitTheme.iconSecondary,
size: 14,
),
),
2025-08-21 14:37:49 +05:30
),
2025-08-24 14:35:17 +05:30
],
),
2025-08-21 23:56:47 +05:30
),
2025-08-21 14:37:49 +05:30
),
);
}
2025-08-24 14:35:17 +05:30
Widget _buildToolTile(
Tool tool,
bool isSelected, {
required VoidCallback onTap,
}) {
return PressableScale(
onTap: onTap,
borderRadius: BorderRadius.circular(AppBorderRadius.md),
child: Container(
margin: const EdgeInsets.only(bottom: Spacing.md),
decoration: BoxDecoration(
gradient: isSelected
? LinearGradient(
colors: [
context.conduitTheme.buttonPrimary.withValues(alpha: 0.2),
context.conduitTheme.buttonPrimary.withValues(alpha: 0.1),
],
)
: null,
2025-08-21 13:02:50 +05:30
color: isSelected
2025-08-24 14:35:17 +05:30
? null
: context.conduitTheme.surfaceBackground.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(AppBorderRadius.md),
border: Border.all(
2025-08-21 13:02:50 +05:30
color: isSelected
2025-08-24 14:35:17 +05:30
? context.conduitTheme.buttonPrimary.withValues(alpha: 0.5)
: context.conduitTheme.dividerColor,
width: BorderWidth.regular,
),
boxShadow: isSelected ? ConduitShadows.card : null,
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: Spacing.md,
vertical: Spacing.sm,
2025-08-21 13:02:50 +05:30
),
2025-08-21 23:56:47 +05:30
child: Row(
2025-08-24 14:35:17 +05:30
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: context.conduitTheme.buttonPrimary.withValues(
alpha: 0.15,
2025-08-21 13:02:50 +05:30
),
2025-08-24 14:35:17 +05:30
borderRadius: BorderRadius.circular(AppBorderRadius.md),
),
child: Icon(
_getToolIcon(tool),
color: context.conduitTheme.buttonPrimary,
size: 16,
),
),
const SizedBox(width: Spacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
2025-08-21 13:02:50 +05:30
Text(
2025-08-24 14:35:17 +05:30
tool.name,
style: TextStyle(
color: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w600,
fontSize: AppTypography.bodyMedium,
2025-08-21 13:02:50 +05:30
),
2025-08-24 14:35:17 +05:30
maxLines: 1,
2025-08-21 13:02:50 +05:30
overflow: TextOverflow.ellipsis,
),
2025-08-24 14:35:17 +05:30
if (tool.meta?['description'] != null &&
(tool.meta!['description'] as String).isNotEmpty) ...[
const SizedBox(height: Spacing.xs),
Text(
tool.meta!['description'],
style: TextStyle(
color: context.conduitTheme.textSecondary,
fontSize: AppTypography.labelSmall,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
],
),
2025-08-21 13:02:50 +05:30
),
2025-08-24 14:35:17 +05:30
const SizedBox(width: Spacing.md),
AnimatedOpacity(
opacity: isSelected ? 1 : 0.6,
duration: AnimationDuration.fast,
child: Container(
padding: const EdgeInsets.all(Spacing.xxs),
decoration: BoxDecoration(
color: isSelected
? context.conduitTheme.buttonPrimary
: context.conduitTheme.surfaceBackground,
borderRadius: BorderRadius.circular(AppBorderRadius.md),
border: Border.all(
color: isSelected
? context.conduitTheme.buttonPrimary.withValues(
alpha: 0.6,
)
: context.conduitTheme.dividerColor,
),
),
child: Icon(
isSelected
? (Platform.isIOS
? CupertinoIcons.check_mark
: Icons.check)
: (Platform.isIOS ? CupertinoIcons.add : Icons.add),
color: isSelected
? context.conduitTheme.textInverse
: context.conduitTheme.iconSecondary,
size: 14,
),
),
),
],
2025-08-21 23:56:47 +05:30
),
2025-08-21 13:02:50 +05:30
),
),
);
}
2025-08-24 14:35:17 +05:30
// Removed small pill builder; using full tiles for consistency
2025-08-20 16:08:44 +05:30
}