Save verified Mithqal AI project progress
This commit is contained in:
@@ -29,6 +29,15 @@ class ApiRepository {
|
||||
return data['model'] as String? ?? 'نموذج غير معروف';
|
||||
}
|
||||
|
||||
Future<List<String>> getAvailableModels() async {
|
||||
final response = await http.get(Uri.parse('$_baseUrl/v1/models'));
|
||||
_checkStatus(response);
|
||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
return (data['data'] as List<dynamic>)
|
||||
.map((model) => (model as Map<String, dynamic>)['id'] as String)
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<String> _userId() async {
|
||||
final cached = _cachedUserId;
|
||||
if (cached != null) return cached;
|
||||
@@ -145,7 +154,26 @@ class ApiRepository {
|
||||
return message['content'] as String? ?? 'لم يصل نص في الإجابة.';
|
||||
}
|
||||
|
||||
Stream<String> completeStream(List<ChatMessage> messages) async* {
|
||||
Future<String> askWorkspaceAgent(String task, {String? model}) async {
|
||||
final response = await http
|
||||
.post(
|
||||
Uri.parse('$_baseUrl/v1/agent/workspace'),
|
||||
headers: const {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({'task': task, if (model != null) 'model': model}),
|
||||
)
|
||||
.timeout(const Duration(minutes: 10));
|
||||
_checkStatus(response);
|
||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final result = data['result'] as String? ?? '';
|
||||
final files = (data['files'] as List<dynamic>? ?? const []).cast<String>();
|
||||
if (files.isEmpty) return result;
|
||||
return '$result\n\nالمصادر المقروءة: ${files.join('، ')}';
|
||||
}
|
||||
|
||||
Stream<String> completeStream(
|
||||
List<ChatMessage> messages, {
|
||||
String? model,
|
||||
}) async* {
|
||||
final client = http.Client();
|
||||
try {
|
||||
final request =
|
||||
@@ -155,6 +183,7 @@ class ApiRepository {
|
||||
'Accept': 'application/x-ndjson',
|
||||
})
|
||||
..body = jsonEncode({
|
||||
if (model != null && model.isNotEmpty) 'model': model,
|
||||
'messages':
|
||||
messages
|
||||
.map(
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:local_notifier/local_notifier.dart';
|
||||
|
||||
class CompletionNotificationService {
|
||||
CompletionNotificationService._();
|
||||
|
||||
static final instance = CompletionNotificationService._();
|
||||
final _plugin = FlutterLocalNotificationsPlugin();
|
||||
bool _initialized = false;
|
||||
|
||||
Future<void> initialize() async {
|
||||
if (kIsWeb || _initialized) return;
|
||||
if (_isDesktop) {
|
||||
await localNotifier.setup(appName: 'Mithqal AI');
|
||||
_initialized = true;
|
||||
return;
|
||||
}
|
||||
const settings = InitializationSettings(
|
||||
android: AndroidInitializationSettings('@mipmap/ic_launcher'),
|
||||
iOS: DarwinInitializationSettings(
|
||||
requestAlertPermission: false,
|
||||
requestBadgePermission: false,
|
||||
requestSoundPermission: false,
|
||||
),
|
||||
);
|
||||
await _plugin.initialize(settings);
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
Future<bool> requestPermission() async {
|
||||
if (kIsWeb) return false;
|
||||
await initialize();
|
||||
if (_isDesktop) return true;
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
return await _plugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin
|
||||
>()
|
||||
?.requestNotificationsPermission() ??
|
||||
true;
|
||||
case TargetPlatform.iOS:
|
||||
return await _plugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
IOSFlutterLocalNotificationsPlugin
|
||||
>()
|
||||
?.requestPermissions(alert: true, badge: true, sound: true) ??
|
||||
false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showFinished({required bool succeeded}) async {
|
||||
if (kIsWeb || !_initialized) return;
|
||||
final title = succeeded ? 'اكتملت الإجابة' : 'تعذر إكمال الطلب';
|
||||
final body = succeeded
|
||||
? 'انتهى النموذج من معالجة طلبك.'
|
||||
: 'افتح التطبيق لمراجعة الخطأ.';
|
||||
if (_isDesktop) {
|
||||
LocalNotification(title: title, body: body).show();
|
||||
return;
|
||||
}
|
||||
const android = AndroidNotificationDetails(
|
||||
'generation_complete',
|
||||
'اكتملت المهام',
|
||||
channelDescription: 'تنبيه عند انتهاء توليد الإجابة أو مهمة الوكيل',
|
||||
importance: Importance.defaultImportance,
|
||||
priority: Priority.defaultPriority,
|
||||
);
|
||||
const details = NotificationDetails(
|
||||
android: android,
|
||||
iOS: DarwinNotificationDetails(),
|
||||
);
|
||||
await _plugin.show(
|
||||
1001,
|
||||
title,
|
||||
body,
|
||||
details,
|
||||
);
|
||||
}
|
||||
|
||||
bool get _isDesktop =>
|
||||
defaultTargetPlatform == TargetPlatform.windows ||
|
||||
defaultTargetPlatform == TargetPlatform.linux ||
|
||||
defaultTargetPlatform == TargetPlatform.macOS;
|
||||
}
|
||||
+109
-8
@@ -14,9 +14,42 @@ class ChatCubit extends Cubit<ChatState> {
|
||||
Future<void> _loadModelName() async {
|
||||
try {
|
||||
final modelName = await _api.getModelName();
|
||||
if (!isClosed) emit(state.copyWith(modelName: modelName));
|
||||
final models = await _api.getAvailableModels();
|
||||
if (!isClosed) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
modelName: modelName,
|
||||
availableModels: models,
|
||||
selectedModel: state.selectedModel ?? modelName,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (_) {
|
||||
if (!isClosed) emit(state.copyWith(modelName: 'تعذر الاتصال بالخادم'));
|
||||
if (!isClosed) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
modelName: 'تعذر الاتصال بالخادم',
|
||||
availableModels: const [],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void selectModel(String model) {
|
||||
if (state.availableModels.contains(model)) {
|
||||
emit(state.copyWith(selectedModel: model, clearError: true));
|
||||
}
|
||||
}
|
||||
|
||||
void toggleWorkspaceMode() {
|
||||
if (!state.isSending) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isWorkspaceMode: !state.isWorkspaceMode,
|
||||
clearError: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,20 +109,79 @@ class ChatCubit extends Cubit<ChatState> {
|
||||
}
|
||||
|
||||
Future<void> send(String text) async {
|
||||
await _sendWithHistory(text, state.messages);
|
||||
}
|
||||
|
||||
Future<void> regenerateLastAnswer() async {
|
||||
if (state.isSending) return;
|
||||
final messages = state.messages;
|
||||
if (messages.length < 2 ||
|
||||
messages.last.fromUser ||
|
||||
!messages[messages.length - 2].fromUser) {
|
||||
return;
|
||||
}
|
||||
final history = messages.sublist(0, messages.length - 1);
|
||||
await _sendWithHistory(
|
||||
history.last.text,
|
||||
history,
|
||||
appendUserMessage: false,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> editLastQuestion(String text) async {
|
||||
if (state.isSending || text.trim().isEmpty) return;
|
||||
final messages = state.messages;
|
||||
final questionIndex = messages.lastIndexWhere(
|
||||
(message) => message.fromUser,
|
||||
);
|
||||
if (questionIndex < 0 || questionIndex != messages.length - 2) return;
|
||||
final history = [
|
||||
...messages.take(questionIndex),
|
||||
ChatMessage(text.trim(), fromUser: true),
|
||||
];
|
||||
await _sendWithHistory(text.trim(), history, appendUserMessage: false);
|
||||
}
|
||||
|
||||
Future<void> _sendWithHistory(
|
||||
String text,
|
||||
List<ChatMessage> history, {
|
||||
bool appendUserMessage = true,
|
||||
}) async {
|
||||
final prompt = text.trim();
|
||||
if (prompt.isEmpty || state.isSending || state.isLoadingHistory) return;
|
||||
final messages = [...state.messages, ChatMessage(prompt, fromUser: true)];
|
||||
final messages =
|
||||
appendUserMessage
|
||||
? [...history, ChatMessage(prompt, fromUser: true)]
|
||||
: List<ChatMessage>.from(history);
|
||||
final originalMessages = state.messages;
|
||||
var answer = '';
|
||||
emit(state.copyWith(messages: messages, isSending: true, clearError: true));
|
||||
try {
|
||||
await _saveCurrent(messages);
|
||||
if (appendUserMessage) await _saveCurrent(messages);
|
||||
if (isClosed) return;
|
||||
emit(
|
||||
state.copyWith(
|
||||
messages: [...messages, const ChatMessage('', fromUser: false)],
|
||||
),
|
||||
);
|
||||
await for (final delta in _api.completeStream(messages)) {
|
||||
if (state.isWorkspaceMode) {
|
||||
answer = await _api.askWorkspaceAgent(
|
||||
prompt,
|
||||
model: state.selectedModel,
|
||||
);
|
||||
if (isClosed) return;
|
||||
if (answer.trim().isEmpty) {
|
||||
throw Exception('لم يصل نص في إجابة الوكيل.');
|
||||
}
|
||||
final updated = [...messages, ChatMessage(answer, fromUser: false)];
|
||||
emit(state.copyWith(messages: updated, isSending: false));
|
||||
await _saveCurrent(updated);
|
||||
return;
|
||||
}
|
||||
await for (final delta in _api.completeStream(
|
||||
messages,
|
||||
model: state.selectedModel,
|
||||
)) {
|
||||
if (isClosed) return;
|
||||
answer += delta;
|
||||
emit(
|
||||
@@ -106,7 +198,9 @@ class ChatCubit extends Cubit<ChatState> {
|
||||
} catch (error) {
|
||||
if (isClosed) return;
|
||||
final partial =
|
||||
answer.isEmpty
|
||||
!appendUserMessage
|
||||
? originalMessages
|
||||
: answer.isEmpty
|
||||
? messages
|
||||
: [...messages, ChatMessage(answer, fromUser: false)];
|
||||
emit(
|
||||
@@ -116,7 +210,7 @@ class ChatCubit extends Cubit<ChatState> {
|
||||
error: error.toString(),
|
||||
),
|
||||
);
|
||||
if (answer.isNotEmpty) {
|
||||
if (appendUserMessage && answer.isNotEmpty) {
|
||||
try {
|
||||
await _saveCurrent(partial);
|
||||
} catch (_) {
|
||||
@@ -181,7 +275,14 @@ class ChatCubit extends Cubit<ChatState> {
|
||||
final url = value.trim();
|
||||
if (url.isEmpty) return;
|
||||
_api.setBaseUrl(url);
|
||||
emit(state.copyWith(apiBaseUrl: _api.baseUrl, clearError: true));
|
||||
emit(
|
||||
state.copyWith(
|
||||
apiBaseUrl: _api.baseUrl,
|
||||
availableModels: const [],
|
||||
clearSelectedModel: true,
|
||||
clearError: true,
|
||||
),
|
||||
);
|
||||
_loadModelName();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,8 +59,11 @@ class ChatState {
|
||||
this.activeConversationId,
|
||||
this.isLoadingHistory = true,
|
||||
this.isSending = false,
|
||||
this.isWorkspaceMode = false,
|
||||
this.error,
|
||||
this.modelName = 'جارٍ اكتشاف النموذج…',
|
||||
this.availableModels = const [],
|
||||
this.selectedModel,
|
||||
required this.apiBaseUrl,
|
||||
});
|
||||
|
||||
@@ -69,8 +72,11 @@ class ChatState {
|
||||
final String? activeConversationId;
|
||||
final bool isLoadingHistory;
|
||||
final bool isSending;
|
||||
final bool isWorkspaceMode;
|
||||
final String? error;
|
||||
final String modelName;
|
||||
final List<String> availableModels;
|
||||
final String? selectedModel;
|
||||
final String apiBaseUrl;
|
||||
|
||||
ChatState copyWith({
|
||||
@@ -80,9 +86,13 @@ class ChatState {
|
||||
bool clearActiveConversation = false,
|
||||
bool? isLoadingHistory,
|
||||
bool? isSending,
|
||||
bool? isWorkspaceMode,
|
||||
String? error,
|
||||
bool clearError = false,
|
||||
String? modelName,
|
||||
List<String>? availableModels,
|
||||
String? selectedModel,
|
||||
bool clearSelectedModel = false,
|
||||
String? apiBaseUrl,
|
||||
}) => ChatState(
|
||||
messages: messages ?? this.messages,
|
||||
@@ -93,8 +103,12 @@ class ChatState {
|
||||
: (activeConversationId ?? this.activeConversationId),
|
||||
isLoadingHistory: isLoadingHistory ?? this.isLoadingHistory,
|
||||
isSending: isSending ?? this.isSending,
|
||||
isWorkspaceMode: isWorkspaceMode ?? this.isWorkspaceMode,
|
||||
error: clearError ? null : (error ?? this.error),
|
||||
modelName: modelName ?? this.modelName,
|
||||
availableModels: availableModels ?? this.availableModels,
|
||||
selectedModel:
|
||||
clearSelectedModel ? null : (selectedModel ?? this.selectedModel),
|
||||
apiBaseUrl: apiBaseUrl ?? this.apiBaseUrl,
|
||||
);
|
||||
}
|
||||
|
||||
+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(
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:record/record.dart';
|
||||
|
||||
import 'core/network/api_repository.dart';
|
||||
import 'core/notifications/completion_notification_service.dart';
|
||||
import 'features/audio/presentation/cubit/voice_cubit.dart';
|
||||
import 'features/chat/presentation/cubit/chat_cubit.dart';
|
||||
import 'features/chat/presentation/pages/chat_page.dart';
|
||||
@@ -10,7 +11,9 @@ import 'features/chat/presentation/pages/chat_page.dart';
|
||||
const _green = Color(0xFF167C68);
|
||||
const _paper = Color(0xFFF6F5F0);
|
||||
|
||||
void main() {
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await CompletionNotificationService.instance.initialize();
|
||||
final api = ApiRepository(
|
||||
baseUrl: const String.fromEnvironment(
|
||||
'API_BASE_URL',
|
||||
@@ -36,7 +39,7 @@ class SovereignApp extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => MaterialApp(
|
||||
title: 'Sovereign AI',
|
||||
title: 'Mithqal AI',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
|
||||
Reference in New Issue
Block a user