didvan-app/lib/views/ai_section/ai_section_page.dart

469 lines
16 KiB
Dart

import 'package:didvan/config/theme_data.dart';
import 'package:didvan/models/ai/ai_chat_args.dart';
import 'package:didvan/models/ai/bots_model.dart';
import 'package:didvan/views/ai/ai_chat_page.dart';
import 'package:didvan/views/ai/ai_chat_state.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/hoshan_home_app_bar.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart';
import 'package:didvan/views/ai/history_ai_chat_state.dart';
import 'package:didvan/views/ai/ai_state.dart';
import 'package:didvan/services/auth/jwt_utils.dart';
import 'package:didvan/services/auth/token_storage.dart';
class AiSectionPage extends StatefulWidget {
const AiSectionPage({super.key});
@override
State<AiSectionPage> createState() => _AiSectionPageState();
}
class _AiSectionGridItem {
final String title;
final String description;
final String iconPath;
final void Function(BuildContext context) onTap;
_AiSectionGridItem({
required this.title,
required this.description,
required this.iconPath,
required this.onTap,
});
}
class _AiSectionPageState extends State<AiSectionPage>
with TickerProviderStateMixin {
late final List<_AiSectionGridItem> _gridItems;
late AnimationController _fadeController;
late AnimationController _slideController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_fadeController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeAnimation = CurvedAnimation(
parent: _fadeController,
curve: Curves.easeIn,
);
_slideController = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _slideController,
curve: Curves.easeOutCubic,
));
_fadeController.forward();
_slideController.forward();
WidgetsBinding.instance.addPostFrameCallback((_) {
final historyAiChatState = context.read<HistoryAiChatState>();
if (historyAiChatState.bots.isEmpty) {
historyAiChatState.getBots();
}
final aiState = context.read<AiState>();
final aiChatState = context.read<AiChatState>();
aiState.onClearChatCallback = () async {
await aiChatState.clearChat();
};
aiState.endChat();
if (aiState.tools == null) {
aiState.getTools();
}
});
_gridItems = [
_AiSectionGridItem(
title: 'ساخت عکس',
description: 'ایجاد تصاویر خلاقانه با هوش مصنوعی',
iconPath: 'lib/assets/icons/Image.svg',
onTap: (context) {
final aiState = context.read<AiState>();
aiState.endChat();
// bot ساخت عکس همیشه id=103 (infoai/Nano Banana 2). دیگر
// وابستهٔ `tools![0]` نیستیم — حتی اگر backend tools را نفرستد
// (یا فرستد ولی bot 103 در index 0 نباشد)، چت ساخت عکس باز
// می‌شود و موقع send درخواست به /103/infoai می‌رود.
BotsModel? infoBot;
try {
for (final t in (aiState.tools ?? const [])) {
for (final b in (t.bots ?? const [])) {
if (b.id == 103) {
infoBot = b;
break;
}
}
if (infoBot != null) break;
}
} catch (_) {/* fall back to hardcoded */}
infoBot ??= (BotsModel(
id: 103,
name: 'infoai',
responseType: 'image',
attachmentType: [],
attachment: 0,
)..short = 'Nano Banana 2');
aiState.startChat(AiChatArgs(bot: infoBot));
},
),
_AiSectionGridItem(
title: 'ترجمه',
description: 'ترجمه فارسی ↔ انگلیسی',
iconPath: 'lib/assets/icons/Translate2.svg',
onTap: (context) {
final aiState = context.read<AiState>();
aiState.endChat();
// مثل ساخت عکس: اول از tools fetched شده دنبال bot id=104
// می‌گردیم؛ اگر نبود یک hardcoded می‌سازیم تا navigate همیشه
// کار کند حتی قبل از آپدیت backend.
BotsModel? translateBot;
try {
for (final t in (aiState.tools ?? const [])) {
for (final b in (t.bots ?? const [])) {
if (b.id == 104) {
translateBot = b;
break;
}
}
if (translateBot != null) break;
}
} catch (_) {/* fall back to hardcoded */}
translateBot ??= (BotsModel(
id: 104,
name: 'translate',
responseType: 'text',
attachmentType: [],
attachment: 0,
)..short = 'مترجم');
aiState.startChat(AiChatArgs(bot: translateBot));
},
),
_AiSectionGridItem(
title: 'خلاصه‌ساز',
description: 'خلاصه‌سازی متن با هوش مصنوعی',
iconPath: 'lib/assets/icons/Summary2.svg',
onTap: (context) {
final aiState = context.read<AiState>();
aiState.endChat();
final aisummeryBot = BotsModel(
id: 100,
name: 'Aisummery',
responseType: 'text',
// attach موقتاً غیرفعال — فقط متن.
attachmentType: [],
attachment: 0,
);
aiState.startChat(AiChatArgs(bot: aisummeryBot));
},
),
_AiSectionGridItem(
title: 'متن به صوت',
description: 'تبدیل متن به فایل صوتی',
iconPath: 'lib/assets/icons/TTS.svg',
onTap: (context) {
final aiState = context.read<AiState>();
aiState.endChat();
final textToSpeechBot = BotsModel(
id: 101,
name: 'aiaudio',
responseType: 'audio',
attachmentType: [],
attachment: 0,
);
aiState.startChat(AiChatArgs(bot: textToSpeechBot));
},
),
_AiSectionGridItem(
title: 'تحلیل نمودار',
description: 'ساخت نمودار با تحلیل هوشمند',
iconPath: 'lib/assets/icons/Nemoudar.svg',
onTap: (context) {
final aiState = context.read<AiState>();
aiState.endChat();
final chartAnalysisBot = BotsModel(
id: 27,
name: 'chart-analysis',
responseType: 'text',
attachmentType: ['image', 'pdf'],
attachment: 1,
);
aiState.startChat(AiChatArgs(bot: chartAnalysisBot));
},
),
_AiSectionGridItem(
title: 'ساخت ویدیو',
description: 'تولید ویدیو با هوش مصنوعی',
iconPath: 'lib/assets/icons/VideoGen.svg',
onTap: (context) {
final aiState = context.read<AiState>();
aiState.endChat();
final aiVideoBot = BotsModel(
id: 102,
name: 'aivideo',
responseType: 'video',
attachmentType: [],
attachment: 0,
);
aiState.startChat(AiChatArgs(bot: aiVideoBot));
},
),
_AiSectionGridItem(
title: 'اینفوگرافی',
description: 'تبدیل محتوا به اینفوگرافی',
iconPath: 'lib/assets/icons/Info.svg',
onTap: (context) {
final aiState = context.read<AiState>();
aiState.endChat();
// بات اینفوگرافی جدا از ساخت عکس است — id=105، Nano Banana 2
// با systemPrompt اینفوگرافی. (id=103 = ساخت عکس عمومی)
final infoAiBot = BotsModel(
id: 105,
name: 'infography',
responseType: 'image',
attachmentType: [],
attachment: 0,
);
aiState.startChat(AiChatArgs(bot: infoAiBot));
},
),
];
}
@override
void dispose() {
_fadeController.dispose();
_slideController.dispose();
super.dispose();
}
Widget _buildAiCategoryItem(
_AiSectionGridItem item, bool isDark, ThemeData theme) {
return Expanded(
child: Padding(
padding: const EdgeInsets.all(4.0),
child: GestureDetector(
onTap: () => item.onTap(context),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: EdgeInsets.zero,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.transparent,
width: 2,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 4),
spreadRadius: 1,
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: SvgPicture.asset(
item.iconPath,
width: 70,
height: 70,
),
),
),
const SizedBox(height: 3),
Text(
item.title,
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(
// در dark mode رنگ روشن، در light mode رنگ تیرهٔ برند.
color: isDark
? const Color.fromARGB(255, 230, 230, 232)
: const Color.fromARGB(255, 27, 60, 89),
fontWeight: FontWeight.bold,
fontSize: 13,
),
),
],
),
),
),
);
}
@override
Widget build(BuildContext context) {
final aiState = context.watch<AiState>();
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
// Multi-tenant: اگر کاربر permission `view:hoshan` ندارد،
// به‌جای صفحه‌ی هوشان یک empty state نشان می‌دهیم.
final token = TokenStorage.accessToken ?? '';
final canUseHoshan = token.isEmpty || JwtUtils.canView(token, 'hoshan');
if (!canUseHoshan) {
return Column(
children: [
const HoshanHomeAppBar(),
Expanded(
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.lock_outline,
size: 72,
color: theme.colorScheme.onSurface.withOpacity(0.25),
),
const SizedBox(height: 16),
DidvanText(
'دسترسی به هوشان فعال نیست',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
DidvanText(
'برای فعال‌سازی دسترسی، با مدیر گروه خود در دیدوان تماس بگیرید.',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
textAlign: TextAlign.center,
),
],
),
),
),
),
],
);
}
return Column(
children: [
if (!aiState.isChatting) const HoshanHomeAppBar(),
Expanded(
child: aiState.isChatting
? AiChatPage(args: aiState.currentChatArgs!, inline: true)
: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FadeTransition(
opacity: _fadeAnimation,
child: Padding(
padding: const EdgeInsets.all(20),
child: Row(
children: [
SvgPicture.asset(
'lib/assets/icons/clarity_tools-line.svg'),
const SizedBox(
width: 8,
),
DidvanText(
'جعبه ابزار هوشان',
style: Theme.of(context).textTheme.titleMedium,
color: Theme.of(context).colorScheme.title,
),
],
),
),
),
SlideTransition(
position: _slideAnimation,
child: FadeTransition(
opacity: _fadeAnimation,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 10),
child: Column(
children: [
if (_gridItems.length >= 3)
Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: _gridItems
.sublist(0, 3)
.map((item) => _buildAiCategoryItem(
item, isDark, theme))
.toList(),
),
const SizedBox(height: 20),
// Second Row (Items 3-6)
if (_gridItems.length >= 6)
Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: _gridItems
.sublist(3, 6)
.map((item) => _buildAiCategoryItem(
item, isDark, theme))
.toList(),
),
const SizedBox(height: 20),
// Third Row (Items 6+)
if (_gridItems.length > 6)
Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
..._gridItems.sublist(6).map((item) =>
_buildAiCategoryItem(
item, isDark, theme)),
if ((_gridItems.length - 6) < 3)
...List.generate(
3 - (_gridItems.length - 6),
(index) =>
const Expanded(child: SizedBox()),
),
],
),
],
),
),
),
),
],
),
),
),
],
);
}
}