Save verified Mithqal AI project progress
This commit is contained in:
+289
-45
@@ -1,11 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_highlight/flutter_highlight.dart';
|
||||
import 'package:flutter_highlight/themes/monokai-sublime.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
import '../../../audio/presentation/cubit/voice_cubit.dart';
|
||||
import '../../../audio/presentation/cubit/voice_state.dart';
|
||||
import '../../../../core/notifications/completion_notification_service.dart';
|
||||
import '../cubit/chat_cubit.dart';
|
||||
import '../cubit/chat_state.dart';
|
||||
|
||||
@@ -22,6 +25,16 @@ class ChatPage extends StatefulWidget {
|
||||
class _ChatPageState extends State<ChatPage> {
|
||||
final _input = TextEditingController();
|
||||
final _scroll = ScrollController();
|
||||
bool _notificationsEnabled = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_notificationsEnabled = !kIsWeb &&
|
||||
(defaultTargetPlatform == TargetPlatform.windows ||
|
||||
defaultTargetPlatform == TargetPlatform.linux ||
|
||||
defaultTargetPlatform == TargetPlatform.macOS);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -108,6 +121,73 @@ class _ChatPageState extends State<ChatPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _copyAnswer(String text) async {
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('تم نسخ الإجابة')));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _shareAnswer(String text) async {
|
||||
await Share.share(text);
|
||||
}
|
||||
|
||||
Future<void> _enableNotifications() async {
|
||||
final allowed =
|
||||
await CompletionNotificationService.instance.requestPermission();
|
||||
if (!mounted) return;
|
||||
setState(() => _notificationsEnabled = allowed);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
kIsWeb
|
||||
? 'إشعارات النظام غير مفعلة في نسخة الويب؛ سيظهر إشعار داخل الصفحة.'
|
||||
: allowed
|
||||
? 'تم تفعيل إشعارات اكتمال المهام.'
|
||||
: 'لم يُمنح إذن الإشعارات؛ ستبقى رسائل الاكتمال داخل التطبيق.',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _editQuestion(String question) async {
|
||||
final controller = TextEditingController(text: question);
|
||||
final revised = await showDialog<String>(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: AlertDialog(
|
||||
title: const Text('تعديل السؤال'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
minLines: 2,
|
||||
maxLines: 6,
|
||||
decoration: const InputDecoration(border: OutlineInputBorder()),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('إلغاء'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed:
|
||||
() => Navigator.pop(context, controller.text.trim()),
|
||||
child: const Text('إعادة الإرسال'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
controller.dispose();
|
||||
if (revised != null && revised.isNotEmpty && mounted) {
|
||||
context.read<ChatCubit>().editLastQuestion(revised);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => MultiBlocListener(
|
||||
listeners: [
|
||||
@@ -122,6 +202,28 @@ class _ChatPageState extends State<ChatPage> {
|
||||
: previous.messages.last.text),
|
||||
listener: (context, state) => _followStreamIfNearby(),
|
||||
),
|
||||
BlocListener<ChatCubit, ChatState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.isSending && !current.isSending,
|
||||
listener: (context, state) {
|
||||
final succeeded = state.error == null;
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
succeeded ? 'اكتملت الإجابة.' : 'انتهى الطلب مع خطأ.',
|
||||
),
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
if (_notificationsEnabled) {
|
||||
CompletionNotificationService.instance.showFinished(
|
||||
succeeded: succeeded,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
BlocListener<VoiceCubit, VoiceState>(
|
||||
listenWhen:
|
||||
(previous, current) =>
|
||||
@@ -178,12 +280,16 @@ class _ChatPageState extends State<ChatPage> {
|
||||
child: const Icon(Icons.hub_rounded, color: Colors.white),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const Text(
|
||||
'Sovereign AI',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: _ink,
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Mithqal AI',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: _ink,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -345,37 +451,105 @@ class _ChatPageState extends State<ChatPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _topBar() => Container(
|
||||
Widget _topBar() => LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final compact = constraints.maxWidth < 640;
|
||||
return Container(
|
||||
height: 70,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 26),
|
||||
padding: EdgeInsets.symmetric(horizontal: compact ? 12 : 26),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(bottom: BorderSide(color: Color(0xFFE7E9E3))),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'مساعدك المحلي',
|
||||
style: TextStyle(
|
||||
color: _ink,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 16,
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'مساعدك المحلي',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: _ink,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
if (!compact) ...[
|
||||
const SizedBox(height: 3),
|
||||
const Text(
|
||||
'المحادثة بالنموذج المحلي · تفريغ الصوت عبر Groq',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: Color(0xFF7A867F), fontSize: 11),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: compact ? 4 : 12),
|
||||
IconButton(
|
||||
onPressed: () => context.read<ChatCubit>().toggleWorkspaceMode(),
|
||||
tooltip:
|
||||
context.watch<ChatCubit>().state.isWorkspaceMode
|
||||
? 'إيقاف وضع الوكيل (قراءة فقط)'
|
||||
: 'وضع الوكيل: قراءة ملفات المشروع فقط',
|
||||
color:
|
||||
context.watch<ChatCubit>().state.isWorkspaceMode ? _green : _ink,
|
||||
icon: Icon(
|
||||
context.watch<ChatCubit>().state.isWorkspaceMode
|
||||
? Icons.folder_open_rounded
|
||||
: Icons.folder_outlined,
|
||||
),
|
||||
),
|
||||
if (context.watch<ChatCubit>().state.availableModels.isNotEmpty)
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: compact ? 112 : 190),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value:
|
||||
context.watch<ChatCubit>().state.availableModels.contains(
|
||||
context.watch<ChatCubit>().state.selectedModel,
|
||||
)
|
||||
? context.watch<ChatCubit>().state.selectedModel
|
||||
: context
|
||||
.watch<ChatCubit>()
|
||||
.state
|
||||
.availableModels
|
||||
.first,
|
||||
isExpanded: true,
|
||||
isDense: true,
|
||||
items:
|
||||
context
|
||||
.watch<ChatCubit>()
|
||||
.state
|
||||
.availableModels
|
||||
.map(
|
||||
(model) => DropdownMenuItem(
|
||||
value: model,
|
||||
child: Text(
|
||||
model,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textDirection: TextDirection.ltr,
|
||||
style: const TextStyle(fontSize: 11),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (model) {
|
||||
if (model != null) {
|
||||
context.read<ChatCubit>().selectModel(model);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(height: 3),
|
||||
Text(
|
||||
'المحادثة بالنموذج المحلي · تفريغ الصوت عبر Groq',
|
||||
style: TextStyle(color: Color(0xFF7A867F), fontSize: 11),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
),
|
||||
SizedBox(width: compact ? 2 : 10),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 7),
|
||||
padding: EdgeInsets.symmetric(horizontal: compact ? 7 : 11, vertical: 7),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFE6F2EC),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
@@ -395,15 +569,29 @@ class _ChatPageState extends State<ChatPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(width: compact ? 2 : 8),
|
||||
IconButton(
|
||||
onPressed: _settings,
|
||||
tooltip: 'إعداد الاتصال',
|
||||
icon: const Icon(Icons.tune_rounded, color: _ink),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _enableNotifications,
|
||||
tooltip: _notificationsEnabled
|
||||
? 'إشعارات اكتمال المهام مفعلة'
|
||||
: 'تفعيل إشعارات اكتمال المهام',
|
||||
color: _notificationsEnabled ? _green : _ink,
|
||||
icon: Icon(
|
||||
_notificationsEnabled
|
||||
? Icons.notifications_active_outlined
|
||||
: Icons.notifications_none_outlined,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Widget _welcome() => Center(
|
||||
child: SingleChildScrollView(
|
||||
@@ -440,9 +628,11 @@ class _ChatPageState extends State<ChatPage> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'اكتب سؤالك، وسأرسله إلى النموذج المحلي على جهازك.',
|
||||
style: TextStyle(color: Color(0xFF718078), fontSize: 14),
|
||||
Text(
|
||||
context.watch<ChatCubit>().state.isWorkspaceMode
|
||||
? 'وضع الوكيل يبحث في ملفات المشروع للقراءة فقط.'
|
||||
: 'اكتب سؤالك، وسأرسله إلى النموذج المحلي على جهازك.',
|
||||
style: const TextStyle(color: Color(0xFF718078), fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
Wrap(
|
||||
@@ -505,18 +695,69 @@ class _ChatPageState extends State<ChatPage> {
|
||||
? null
|
||||
: Border.all(color: const Color(0xFFE8EAE5)),
|
||||
),
|
||||
child:
|
||||
message.fromUser
|
||||
? SelectableText(
|
||||
message.text,
|
||||
textDirection: TextDirection.rtl,
|
||||
style: const TextStyle(
|
||||
color: _ink,
|
||||
fontSize: 14,
|
||||
height: 1.65,
|
||||
),
|
||||
)
|
||||
: _AssistantMarkdown(text: message.text),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (message.fromUser)
|
||||
SelectableText(
|
||||
message.text,
|
||||
textDirection: TextDirection.rtl,
|
||||
style: const TextStyle(
|
||||
color: _ink,
|
||||
fontSize: 14,
|
||||
height: 1.65,
|
||||
),
|
||||
)
|
||||
else
|
||||
_AssistantMarkdown(text: message.text),
|
||||
if (!chat.isSending &&
|
||||
!showTyping &&
|
||||
index == chat.messages.length - 1 &&
|
||||
!message.fromUser &&
|
||||
message.text.isNotEmpty)
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Wrap(
|
||||
spacing: 2,
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: 'نسخ الإجابة',
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: () => _copyAnswer(message.text),
|
||||
icon: const Icon(Icons.copy_rounded, size: 18),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'مشاركة الإجابة',
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: () => _shareAnswer(message.text),
|
||||
icon: const Icon(Icons.share_rounded, size: 18),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'إعادة توليد الإجابة',
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed:
|
||||
() =>
|
||||
context
|
||||
.read<ChatCubit>()
|
||||
.regenerateLastAnswer(),
|
||||
icon: const Icon(Icons.refresh_rounded, size: 19),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else if (!chat.isSending &&
|
||||
index == chat.messages.length - 2 &&
|
||||
message.fromUser)
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: TextButton.icon(
|
||||
onPressed: () => _editQuestion(message.text),
|
||||
icon: const Icon(Icons.edit_outlined, size: 16),
|
||||
label: const Text('تعديل السؤال'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -582,8 +823,11 @@ class _ChatPageState extends State<ChatPage> {
|
||||
textDirection: TextDirection.rtl,
|
||||
textInputAction: TextInputAction.send,
|
||||
onSubmitted: (_) => _send(),
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'اكتب رسالتك هنا…',
|
||||
decoration: InputDecoration(
|
||||
hintText:
|
||||
chat.isWorkspaceMode
|
||||
? 'اسأل عن ملفات مساحة العمل…'
|
||||
: 'اكتب رسالتك هنا…',
|
||||
hintStyle: TextStyle(color: Color(0xFF9BA59E)),
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
|
||||
Reference in New Issue
Block a user