Save verified Mithqal AI project progress

This commit is contained in:
Hamza Ayed
2026-10-01 01:02:03 +03:00
parent f0a444ca46
commit e1f981d29e
111 changed files with 1589 additions and 295 deletions
@@ -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(