From ec9d74173f053ed96440d8e6c99ba7474d6f028a Mon Sep 17 00:00:00 2001 From: Hamza Ayed Date: Sun, 4 Oct 2026 18:30:40 +0300 Subject: [PATCH] fix web project folder actions --- SovereignAI-Starter/ROADMAP.md | 2 + .../core/projects/project_folder_service.dart | 5 ++ .../chat/presentation/pages/chat_page.dart | 86 +++++++++++++++---- .../test/project_folder_service_test.dart | 15 ++++ 4 files changed, 93 insertions(+), 15 deletions(-) diff --git a/SovereignAI-Starter/ROADMAP.md b/SovereignAI-Starter/ROADMAP.md index 0117780..e7e7c29 100644 --- a/SovereignAI-Starter/ROADMAP.md +++ b/SovereignAI-Starter/ROADMAP.md @@ -4,6 +4,8 @@ ## الحالة الحالية — 2026-10-04 +- 2026-10-04 — إصلاح إجراء المشروع غير المستجيب على Flutter Web: مراجعة `file_selector_web 0.9.4+2` المثبت محليًا أكدت أن `getDirectoryPath()` يعيد `null` على الويب؛ لذلك لم يكن زر تسجيل مجلد كامل قادرًا على فتح المنتقي. حُوّل الإجراء الرئيسي في الويب إلى اختيار ملفات كود/PDF للتحليل، وأُخفي إجراء تسجيل المجلد من قائمة «مشاريعي» عندما لا يوجد مشروع مسجل، مع رسالة تشرح خيار الملفات والمشاريع المسجلة. أزرار تسجيل المجلد بقيت لسطح المكتب، وقائمة المشاريع المسجلة سابقًا تظل قابلة للاختيار. اختبار Flutter الكامل 25/25، `flutter analyze --no-pub` بلا ملاحظات، وبناء Web نجح خلال 171.3 ثانية. لم يتم بعد اختبار إرسال ملف فعلي من نافذة المتصفح ولا التحقق المرئي من Windows؛ لا تُعد هذه الخطوة تسجيل مشروع كاملًا في الويب. + - 2026-10-04 — اختبار عقد بث الوكيل في عميل Flutter: أُضيف خادم HTTP محلي للاختبار يرسل أحداث تقدم و`heartbeat` ثم `done`، ويتحقق من عرض رسالة النبضة والجواب النهائي وتمرير Bearer ومساحة المشروع والملفات المختارة والمهارة في الطلب. اختبار الملف المستهدف نجح 3/3، ومجموعة Flutter الكاملة نجحت 24/24، و`flutter analyze --no-pub` بلا ملاحظات. هذا يثبت فك أحداث SSE في عميل Dart ولا يغني عن تجربة النافذة يدويًا أو عن طلب حي طويل؛ مشكلة عدم ظهور نافذة نسخة LocalAppData لواجهة التحكم موثقة في بند التحقق المرئي أدناه. - 2026-10-04 — إعادة فحص آمنة لبوابة حصة قرص الوكيل: شُغّل `scripts/verify_vhd_disk_quota.ps1`؛ مسار UAC المعتاد فشل قبل الإقلاع بـ`0xc0000142`. تجربة التنفيذ خارج sandbox لم تُظهر عملية PowerShell مرتفعة أو DiskPart حتى بعد الانتظار، فأُوقفت؛ فحص القراءة فقط أكد عدم وجود مجلد probe أو عملية DiskPart جديدة. لم يُنشأ أو يُركّب VHDX، ولم تتغير الأقراص. ما زال إثبات حصة القرص يتطلب جلسة إدارية تفاعلية تعمل. diff --git a/SovereignAI-Starter/flutter_app/lib/core/projects/project_folder_service.dart b/SovereignAI-Starter/flutter_app/lib/core/projects/project_folder_service.dart index 27d0d67..c3160c4 100644 --- a/SovereignAI-Starter/flutter_app/lib/core/projects/project_folder_service.dart +++ b/SovereignAI-Starter/flutter_app/lib/core/projects/project_folder_service.dart @@ -2,6 +2,11 @@ import 'project_folder_stub.dart' if (dart.library.io) 'project_folder_io.dart' as platform; +String? workspaceFolderPickerUnavailableMessage({required bool isWeb}) => + isWeb + ? 'إضافة مجلد كامل غير مدعومة في نسخة الويب الحالية. أرفق ملفات محددة للتحليل، أو افتح مشروعًا مسجلًا مسبقًا من «مشاريعي». لتسجيل مجلد جديد استخدم تطبيق سطح المكتب.' + : null; + bool get canCreateProjectFolders => platform.canCreateProjectFolders; Future createProjectFolder(String parentPath, String projectName) => diff --git a/SovereignAI-Starter/flutter_app/lib/features/chat/presentation/pages/chat_page.dart b/SovereignAI-Starter/flutter_app/lib/features/chat/presentation/pages/chat_page.dart index e23be6c..86a37f7 100644 --- a/SovereignAI-Starter/flutter_app/lib/features/chat/presentation/pages/chat_page.dart +++ b/SovereignAI-Starter/flutter_app/lib/features/chat/presentation/pages/chat_page.dart @@ -105,10 +105,33 @@ class _ChatPageState extends State { context.read().toggleWorkspaceMode(); return; } + if (kIsWeb) { + final state = context.read().state; + final message = workspaceFolderPickerUnavailableMessage(isWeb: true)!; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + state.savedWorkspacePaths.isEmpty + ? message + : 'اختر مشروعًا مسجلًا من قائمة «مشاريعي»، أو أرفق ملفات محددة للتحليل.', + ), + ), + ); + return; + } await _addWorkspaceProject(); } Future _addWorkspaceProject() async { + final unsupportedMessage = workspaceFolderPickerUnavailableMessage( + isWeb: kIsWeb, + ); + if (unsupportedMessage != null) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(unsupportedMessage))); + return; + } try { final directory = await getDirectoryPath( confirmButtonText: 'تسجيل هذا المشروع', @@ -1176,15 +1199,24 @@ class _ChatPageState extends State { ), ), ), - IconButton( - tooltip: 'إضافة مشروع موجود إلى القائمة', - visualDensity: VisualDensity.compact, - onPressed: chat.isSending ? null : _addWorkspaceProject, - icon: const Icon(Icons.create_new_folder_outlined, size: 19), - ), + if (!kIsWeb) + IconButton( + tooltip: 'إضافة مشروع موجود إلى القائمة', + visualDensity: VisualDensity.compact, + onPressed: chat.isSending ? null : _addWorkspaceProject, + icon: const Icon(Icons.create_new_folder_outlined, size: 19), + ), ], ), - if (chat.savedWorkspacePaths.isEmpty) + if (chat.savedWorkspacePaths.isEmpty && kIsWeb) + const Padding( + padding: EdgeInsets.symmetric(horizontal: 4, vertical: 6), + child: Text( + 'سجّل المجلد من تطبيق سطح المكتب، أو أرفق ملفات محددة من زر المشبك.', + style: TextStyle(color: Color(0xFF718078), fontSize: 12), + ), + ) + else if (chat.savedWorkspacePaths.isEmpty) TextButton.icon( onPressed: chat.isSending ? null : _addWorkspaceProject, icon: const Icon(Icons.add, size: 16), @@ -1487,7 +1519,19 @@ class _ChatPageState extends State { ), ), SizedBox(width: compact ? 2 : 12), - if (constraints.maxWidth >= 900) + if (kIsWeb && constraints.maxWidth >= 900) + FilledButton.tonalIcon( + onPressed: chat.isSending ? null : _pickCodeFiles, + icon: const Icon(Icons.attach_file_rounded), + label: const Text('إرفاق ملفات للتحليل'), + ) + else if (kIsWeb) + IconButton.filledTonal( + onPressed: chat.isSending ? null : _pickCodeFiles, + tooltip: 'إرفاق ملفات للتحليل', + icon: const Icon(Icons.attach_file_rounded), + ) + else if (constraints.maxWidth >= 900) FilledButton.tonalIcon( onPressed: chat.isSending ? null : _addWorkspaceProject, icon: const Icon(Icons.create_new_folder_outlined), @@ -1915,7 +1959,9 @@ class _ChatPageState extends State { ), const SizedBox(height: 6), Text( - chat.savedWorkspacePaths.isEmpty + kIsWeb + ? 'في نسخة الويب، أرفق ملفات محددة عبر زر المشبك أو اختر مشروعًا سبق تسجيله. تسجيل مجلد كامل متاح في تطبيق سطح المكتب.' + : chat.savedWorkspacePaths.isEmpty ? 'سجّل مجلد مشروع موجود على جهازك؛ بعدها يقرأ الوكيل الملفات التي تختارها ويجيب عنها.' : 'اختر مجلد مشروع مسجلًا من «مشاريعي»، أو سجّل مجلدًا موجودًا جديدًا ليقرأ الوكيل ملفاته.', style: const TextStyle( @@ -1928,12 +1974,22 @@ class _ChatPageState extends State { spacing: 10, runSpacing: 8, children: [ - FilledButton.icon( - onPressed: - chat.isSending ? null : _addWorkspaceProject, - icon: const Icon(Icons.folder_open_outlined), - label: const Text('فتح مجلد مشروع موجود'), - ), + if (kIsWeb) + FilledButton.icon( + onPressed: + chat.isSending ? null : _pickCodeFiles, + icon: const Icon(Icons.attach_file_rounded), + label: const Text('إرفاق ملفات للتحليل'), + ) + else + FilledButton.icon( + onPressed: + chat.isSending + ? null + : _addWorkspaceProject, + icon: const Icon(Icons.folder_open_outlined), + label: const Text('فتح مجلد مشروع موجود'), + ), if (canCreateProjectFolders) OutlinedButton.icon( onPressed: diff --git a/SovereignAI-Starter/flutter_app/test/project_folder_service_test.dart b/SovereignAI-Starter/flutter_app/test/project_folder_service_test.dart index a064f85..13374d0 100644 --- a/SovereignAI-Starter/flutter_app/test/project_folder_service_test.dart +++ b/SovereignAI-Starter/flutter_app/test/project_folder_service_test.dart @@ -4,6 +4,21 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_app/core/projects/project_folder_service.dart'; void main() { + test( + 'explains that browser folder registration uses selected file uploads', + () { + expect( + workspaceFolderPickerUnavailableMessage(isWeb: true), + contains('أرفق ملفات محددة'), + ); + expect( + workspaceFolderPickerUnavailableMessage(isWeb: true), + contains('تطبيق سطح المكتب'), + ); + expect(workspaceFolderPickerUnavailableMessage(isWeb: false), isNull); + }, + ); + test('creates a new project folder under the chosen parent', () async { final parent = await Directory.systemTemp.createTemp( 'mithqal-project-parent-',