Complete local hybrid search and improve agent reliability
This commit is contained in:
+367
-22
@@ -1,18 +1,22 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:file_selector/file_selector.dart';
|
||||
|
||||
import '../../../../core/network/api_repository.dart';
|
||||
import '../../../../core/settings/local_settings_store.dart';
|
||||
import 'chat_state.dart';
|
||||
|
||||
class ChatCubit extends Cubit<ChatState> {
|
||||
ChatCubit(this._api) : super(ChatState(apiBaseUrl: _api.baseUrl)) {
|
||||
_loadHistory();
|
||||
_loadModelName();
|
||||
ChatCubit(this._api, {LocalSettingsStore? settingsStore})
|
||||
: _settingsStore = settingsStore ?? LocalSettingsStore(),
|
||||
super(ChatState(apiBaseUrl: _api.baseUrl)) {
|
||||
_initialize();
|
||||
}
|
||||
|
||||
final ApiRepository _api;
|
||||
final LocalSettingsStore _settingsStore;
|
||||
Timer? _elapsedTimer;
|
||||
Stopwatch? _requestStopwatch;
|
||||
int _requestGeneration = 0;
|
||||
@@ -20,6 +24,94 @@ class ChatCubit extends Cubit<ChatState> {
|
||||
bool _requestAppendedUserMessage = true;
|
||||
bool _requestHasProducedText = false;
|
||||
|
||||
bool get _defaultNotificationsEnabled =>
|
||||
!kIsWeb &&
|
||||
(defaultTargetPlatform == TargetPlatform.windows ||
|
||||
defaultTargetPlatform == TargetPlatform.linux ||
|
||||
defaultTargetPlatform == TargetPlatform.macOS);
|
||||
|
||||
Future<void> _initialize() async {
|
||||
try {
|
||||
final settings = await _settingsStore.read();
|
||||
final savedApiUrl = settings['api_base_url'];
|
||||
if (savedApiUrl is String && _isValidApiUrl(savedApiUrl)) {
|
||||
_api.setBaseUrl(savedApiUrl);
|
||||
}
|
||||
final missingDefaults = <String, Object?>{
|
||||
if (savedApiUrl is! String || !_isValidApiUrl(savedApiUrl))
|
||||
'api_base_url': _api.baseUrl,
|
||||
if (settings['notifications_enabled'] is! bool)
|
||||
'notifications_enabled': _defaultNotificationsEnabled,
|
||||
if (settings['dark_mode'] is! bool) 'dark_mode': false,
|
||||
};
|
||||
if (missingDefaults.isNotEmpty) {
|
||||
await _persistSettings(missingDefaults);
|
||||
}
|
||||
final savedModel = settings['selected_model'];
|
||||
emit(
|
||||
state.copyWith(
|
||||
apiBaseUrl: _api.baseUrl,
|
||||
selectedModel: savedModel is String ? savedModel : null,
|
||||
notificationsEnabled:
|
||||
settings['notifications_enabled'] is bool
|
||||
? settings['notifications_enabled']! as bool
|
||||
: _defaultNotificationsEnabled,
|
||||
isDarkMode:
|
||||
settings['dark_mode'] is bool
|
||||
? settings['dark_mode']! as bool
|
||||
: false,
|
||||
),
|
||||
);
|
||||
if (!_settingsStore.persistenceAvailable) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
error:
|
||||
'التخزين المحلي الدائم غير متاح؛ ستبقى الإعدادات افتراضية لهذه الجلسة.',
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
notificationsEnabled: _defaultNotificationsEnabled,
|
||||
error:
|
||||
'تعذر تحميل الإعدادات المحلية؛ ستُستخدم القيم الافتراضية: $error',
|
||||
),
|
||||
);
|
||||
}
|
||||
await Future.wait([_loadHistory(), _loadModelName(), _loadAgentSkills()]);
|
||||
}
|
||||
|
||||
bool _isValidApiUrl(String value) {
|
||||
final uri = Uri.tryParse(value.trim());
|
||||
return uri != null &&
|
||||
(uri.scheme == 'http' || uri.scheme == 'https') &&
|
||||
uri.host.isNotEmpty &&
|
||||
uri.userInfo.isEmpty &&
|
||||
!uri.hasQuery &&
|
||||
!uri.hasFragment;
|
||||
}
|
||||
|
||||
Future<bool> _persistSettings(Map<String, Object?> values) async {
|
||||
try {
|
||||
final saved = await _settingsStore.update(values);
|
||||
if (!saved && !isClosed) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
error:
|
||||
'تعذر حفظ الإعدادات بعد إغلاق التطبيق؛ يعمل الإعداد الحالي لهذه الجلسة فقط.',
|
||||
),
|
||||
);
|
||||
}
|
||||
return saved;
|
||||
} catch (error) {
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(error: 'تعذر حفظ الإعدادات المحلية: $error'));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void _startElapsedTimer(int generation) {
|
||||
_elapsedTimer?.cancel();
|
||||
_requestStopwatch = Stopwatch()..start();
|
||||
@@ -56,14 +148,32 @@ class ChatCubit extends Cubit<ChatState> {
|
||||
final modelName = await _api.getModelName();
|
||||
final models = await _api.getAvailableModels();
|
||||
if (!isClosed) {
|
||||
final chatModels =
|
||||
models
|
||||
.where(
|
||||
(model) =>
|
||||
!model.capabilitiesVerified ||
|
||||
model.capabilities.contains('completion'),
|
||||
)
|
||||
.toList();
|
||||
final selectableModels = chatModels.isEmpty ? models : chatModels;
|
||||
final previousSelection = state.selectedModel;
|
||||
final modelIds = selectableModels.map((model) => model.id).toList();
|
||||
final selectedModel =
|
||||
modelIds.contains(previousSelection)
|
||||
? previousSelection!
|
||||
: modelName;
|
||||
emit(
|
||||
state.copyWith(
|
||||
modelName: modelName,
|
||||
modelStatus: ModelConnectionStatus.connected,
|
||||
availableModels: models,
|
||||
selectedModel: state.selectedModel ?? modelName,
|
||||
availableModels: selectableModels,
|
||||
selectedModel: selectedModel,
|
||||
),
|
||||
);
|
||||
if (selectedModel != previousSelection) {
|
||||
await _persistSettings({'selected_model': selectedModel});
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
if (!isClosed) {
|
||||
@@ -78,30 +188,211 @@ class ChatCubit extends Cubit<ChatState> {
|
||||
}
|
||||
}
|
||||
|
||||
void selectModel(String model) {
|
||||
if (state.availableModels.contains(model)) {
|
||||
emit(state.copyWith(selectedModel: model, clearError: true));
|
||||
Future<void> _loadAgentSkills() async {
|
||||
try {
|
||||
final available = await _api.getAgentSkills();
|
||||
if (!isClosed) emit(state.copyWith(availableSkills: available));
|
||||
} catch (error) {
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(error: 'تعذر تحميل مهارات الوكيل: $error'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void selectAgentSkill(String? skillId) {
|
||||
if (state.isSending) return;
|
||||
if (skillId != null &&
|
||||
!state.availableSkills.any((skill) => skill.id == skillId)) {
|
||||
return;
|
||||
}
|
||||
emit(
|
||||
state.copyWith(
|
||||
selectedSkillId: skillId,
|
||||
clearSelectedSkill: skillId == null,
|
||||
clearError: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void selectModel(String model) {
|
||||
if (state.availableModels.any((descriptor) => descriptor.id == model)) {
|
||||
emit(state.copyWith(selectedModel: model, clearError: true));
|
||||
unawaited(_persistSettings({'selected_model': model}));
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> setNotificationsEnabled(bool enabled) async {
|
||||
emit(state.copyWith(notificationsEnabled: enabled, clearError: true));
|
||||
return _persistSettings({'notifications_enabled': enabled});
|
||||
}
|
||||
|
||||
Future<bool> setDarkMode(bool enabled) async {
|
||||
emit(state.copyWith(isDarkMode: enabled, clearError: true));
|
||||
return _persistSettings({'dark_mode': enabled});
|
||||
}
|
||||
|
||||
void toggleWorkspaceMode() {
|
||||
if (!state.isSending) {
|
||||
final enable = !state.isWorkspaceMode;
|
||||
if (enable && state.selectedWorkspacePath == null) return;
|
||||
emit(
|
||||
state.copyWith(
|
||||
isWorkspaceMode: !state.isWorkspaceMode,
|
||||
isWorkspaceMode: enable,
|
||||
isWebSearchMode: false,
|
||||
clearSelectedWorkspacePath: !enable,
|
||||
availableWorkspaceFiles:
|
||||
enable ? state.availableWorkspaceFiles : const [],
|
||||
selectedWorkspaceFiles:
|
||||
enable ? state.selectedWorkspaceFiles : const [],
|
||||
clearError: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<String>> selectWorkspace(String path) async {
|
||||
if (state.isSending || path.trim().isEmpty) return const [];
|
||||
emit(
|
||||
state.copyWith(
|
||||
selectedWorkspacePath: path,
|
||||
isWorkspaceMode: true,
|
||||
isWebSearchMode: false,
|
||||
availableWorkspaceFiles: const [],
|
||||
selectedWorkspaceFiles: const [],
|
||||
clearError: true,
|
||||
),
|
||||
);
|
||||
try {
|
||||
final files = await _api.listWorkspaceFiles(path);
|
||||
if (isClosed || state.selectedWorkspacePath != path) return const [];
|
||||
emit(state.copyWith(availableWorkspaceFiles: files));
|
||||
return files;
|
||||
} catch (error) {
|
||||
if (!isClosed) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isWorkspaceMode: false,
|
||||
clearSelectedWorkspacePath: true,
|
||||
error: _readableError(error),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
|
||||
void selectWorkspaceFiles(List<String> paths) {
|
||||
if (state.isSending || paths.length > 3) return;
|
||||
final available = state.availableWorkspaceFiles.toSet();
|
||||
if (paths.any((path) => !available.contains(path))) return;
|
||||
emit(state.copyWith(selectedWorkspaceFiles: List.unmodifiable(paths)));
|
||||
}
|
||||
|
||||
Future<String> indexSelectedWorkspaceKnowledge() async {
|
||||
if (state.isSending) throw StateError('انتظر حتى يكتمل طلب الوكيل الحالي.');
|
||||
final path = state.selectedWorkspacePath;
|
||||
final files = state.selectedWorkspaceFiles;
|
||||
if (path == null || files.isEmpty) {
|
||||
throw StateError('اختر مساحة عمل وحدد ملفًا واحدًا على الأقل للفهرسة.');
|
||||
}
|
||||
emit(state.copyWith(agentProgress: 'يفهرس الملفات المحددة محليًا…'));
|
||||
try {
|
||||
final result = await _api.indexWorkspaceKnowledge(path, files);
|
||||
final indexed = result['indexed'] as List<dynamic>? ?? const [];
|
||||
final chunks = indexed.fold<int>(
|
||||
0,
|
||||
(total, item) =>
|
||||
total + ((item as Map<String, dynamic>)['chunks'] as int? ?? 0),
|
||||
);
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(clearAgentProgress: true, clearError: true));
|
||||
}
|
||||
return 'فُهرست ${indexed.length} ملفًا محليًا في SQLite ($chunks مقطعًا).';
|
||||
} catch (error) {
|
||||
final message = _readableError(error);
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(clearAgentProgress: true, error: message));
|
||||
}
|
||||
throw Exception(message);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> removeSelectedWorkspaceKnowledge() async {
|
||||
if (state.isSending) throw StateError('انتظر حتى يكتمل طلب الوكيل الحالي.');
|
||||
final path = state.selectedWorkspacePath;
|
||||
final files = state.selectedWorkspaceFiles;
|
||||
if (path == null || files.isEmpty) {
|
||||
throw StateError('اختر مساحة عمل وحدد ملفًا واحدًا على الأقل.');
|
||||
}
|
||||
emit(
|
||||
state.copyWith(agentProgress: 'يحذف الملفات المحددة من الفهرس المحلي…'),
|
||||
);
|
||||
try {
|
||||
final result = await _api.deleteWorkspaceKnowledge(path, files);
|
||||
final deleted =
|
||||
(result['deleted'] as List<dynamic>? ?? const [])
|
||||
.where(
|
||||
(item) => (item as Map<String, dynamic>)['deleted'] == true,
|
||||
)
|
||||
.length;
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(clearAgentProgress: true, clearError: true));
|
||||
}
|
||||
return deleted == 0
|
||||
? 'لم تكن الملفات المحددة موجودة في فهرس المعرفة.'
|
||||
: 'أُزيل $deleted ملف من فهرس المعرفة المحلي.';
|
||||
} catch (error) {
|
||||
final message = _readableError(error);
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(clearAgentProgress: true, error: message));
|
||||
}
|
||||
throw Exception(message);
|
||||
}
|
||||
}
|
||||
|
||||
void dismissFileChangeProposal() {
|
||||
if (state.pendingFileChangeProposal != null) {
|
||||
emit(state.copyWith(clearPendingFileChangeProposal: true));
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> applyFileChangeProposal() async {
|
||||
final proposal = state.pendingFileChangeProposal;
|
||||
if (proposal == null) throw StateError('لا توجد معاينة تغيير معلقة.');
|
||||
emit(state.copyWith(agentProgress: 'يطبق التغيير الذي وافقت عليه.'));
|
||||
try {
|
||||
final message = await _api.applyFileChange(proposal);
|
||||
if (!isClosed) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
clearPendingFileChangeProposal: true,
|
||||
clearAgentProgress: true,
|
||||
clearError: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
return message;
|
||||
} catch (error) {
|
||||
if (!isClosed) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
clearPendingFileChangeProposal: true,
|
||||
clearAgentProgress: true,
|
||||
error: _readableError(error),
|
||||
),
|
||||
);
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
void toggleWebSearchMode() {
|
||||
if (!state.isSending) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isWebSearchMode: !state.isWebSearchMode,
|
||||
isWorkspaceMode: false,
|
||||
clearSelectedWorkspacePath: true,
|
||||
clearError: true,
|
||||
),
|
||||
);
|
||||
@@ -239,9 +530,10 @@ class ChatCubit extends Cubit<ChatState> {
|
||||
);
|
||||
final feedback = List<int?>.generate(
|
||||
message.answerVersions.length,
|
||||
(index) => index < message.feedbackVersions.length
|
||||
? message.feedbackVersions[index]
|
||||
: null,
|
||||
(index) =>
|
||||
index < message.feedbackVersions.length
|
||||
? message.feedbackVersions[index]
|
||||
: null,
|
||||
)..[message.selectedVersion] = rating;
|
||||
messages[messageIndex] = ChatMessage(
|
||||
message.text,
|
||||
@@ -296,11 +588,14 @@ class ChatCubit extends Cubit<ChatState> {
|
||||
_requestAppendedUserMessage = appendUserMessage;
|
||||
_requestHasProducedText = false;
|
||||
var answer = '';
|
||||
FileChangeProposal? pendingFileChangeProposal;
|
||||
final generation = ++_requestGeneration;
|
||||
emit(
|
||||
state.copyWith(
|
||||
messages: messages,
|
||||
isSending: true,
|
||||
clearAgentProgress: true,
|
||||
clearPendingFileChangeProposal: true,
|
||||
requestStatus: RequestStatus.generating,
|
||||
elapsed: Duration.zero,
|
||||
clearLastResponseDuration: true,
|
||||
@@ -316,16 +611,58 @@ class ChatCubit extends Cubit<ChatState> {
|
||||
),
|
||||
);
|
||||
_startElapsedTimer(generation);
|
||||
if (attachments.isNotEmpty || state.isWorkspaceMode || state.isWebSearchMode) {
|
||||
if (attachments.isNotEmpty ||
|
||||
state.isWorkspaceMode ||
|
||||
state.isWebSearchMode) {
|
||||
const imageExtensions = {'png', 'jpg', 'jpeg', 'webp'};
|
||||
final hasImages = attachments.any((file) {
|
||||
final extension = file.name.split('.').last.toLowerCase();
|
||||
return imageExtensions.contains(extension);
|
||||
});
|
||||
final hasNonImages = attachments.any((file) {
|
||||
final extension = file.name.split('.').last.toLowerCase();
|
||||
return !imageExtensions.contains(extension);
|
||||
});
|
||||
if (hasImages && hasNonImages) {
|
||||
throw Exception(
|
||||
'أرسل الصور وحدها، أو أرسل ملفات الكود وحدها في رسالة أخرى.',
|
||||
);
|
||||
}
|
||||
answer =
|
||||
attachments.isNotEmpty
|
||||
? await _api.analyzeFiles(
|
||||
attachments,
|
||||
prompt.isEmpty ? 'حلّل الملفات المرفقة واشرح وظيفتها.' : prompt,
|
||||
model: state.selectedModel,
|
||||
)
|
||||
? hasImages
|
||||
? await _api.analyzeImages(
|
||||
attachments,
|
||||
prompt.isEmpty
|
||||
? 'اقرأ النصوص في الصورة واشرح محتواها.'
|
||||
: prompt,
|
||||
model: state.selectedModel,
|
||||
)
|
||||
: await _api.analyzeFiles(
|
||||
attachments,
|
||||
prompt.isEmpty
|
||||
? 'حلّل الملفات المرفقة واشرح وظيفتها.'
|
||||
: prompt,
|
||||
model: state.selectedModel,
|
||||
)
|
||||
: state.isWorkspaceMode
|
||||
? await _api.askWorkspaceAgent(prompt, model: state.selectedModel)
|
||||
? await _api
|
||||
.runAgent(
|
||||
prompt,
|
||||
model: state.selectedModel,
|
||||
workspacePath: state.selectedWorkspacePath,
|
||||
workspaceFiles: state.selectedWorkspaceFiles,
|
||||
skillId: state.selectedSkillId,
|
||||
onProgress: (message) {
|
||||
if (!isClosed && generation == _requestGeneration) {
|
||||
emit(state.copyWith(agentProgress: message));
|
||||
}
|
||||
},
|
||||
)
|
||||
.then((result) {
|
||||
pendingFileChangeProposal = result.proposal;
|
||||
return result.answer;
|
||||
})
|
||||
: await _api.searchWeb(prompt, model: state.selectedModel);
|
||||
if (isClosed || generation != _requestGeneration) return;
|
||||
if (answer.trim().isEmpty) {
|
||||
@@ -348,8 +685,10 @@ class ChatCubit extends Cubit<ChatState> {
|
||||
messages: updated,
|
||||
isSending: false,
|
||||
requestStatus: RequestStatus.completed,
|
||||
clearAgentProgress: true,
|
||||
elapsed: duration,
|
||||
lastResponseDuration: duration,
|
||||
pendingFileChangeProposal: pendingFileChangeProposal,
|
||||
),
|
||||
);
|
||||
await _saveCurrent(updated);
|
||||
@@ -425,6 +764,8 @@ class ChatCubit extends Cubit<ChatState> {
|
||||
messages: partial,
|
||||
isSending: false,
|
||||
requestStatus: RequestStatus.failed,
|
||||
clearAgentProgress: true,
|
||||
clearPendingFileChangeProposal: true,
|
||||
elapsed: duration,
|
||||
lastResponseDuration: duration,
|
||||
error: _readableError(error),
|
||||
@@ -459,6 +800,8 @@ class ChatCubit extends Cubit<ChatState> {
|
||||
messages: messages,
|
||||
isSending: false,
|
||||
requestStatus: RequestStatus.cancelled,
|
||||
clearAgentProgress: true,
|
||||
clearPendingFileChangeProposal: true,
|
||||
elapsed: duration,
|
||||
lastResponseDuration: duration,
|
||||
clearError: true,
|
||||
@@ -539,9 +882,9 @@ class ChatCubit extends Cubit<ChatState> {
|
||||
}
|
||||
}
|
||||
|
||||
void setApiBaseUrl(String value) {
|
||||
Future<bool> setApiBaseUrl(String value) async {
|
||||
final url = value.trim();
|
||||
if (url.isEmpty) return;
|
||||
if (url.isEmpty || !_isValidApiUrl(url) || state.isSending) return false;
|
||||
_api.setBaseUrl(url);
|
||||
emit(
|
||||
state.copyWith(
|
||||
@@ -552,6 +895,8 @@ class ChatCubit extends Cubit<ChatState> {
|
||||
clearError: true,
|
||||
),
|
||||
);
|
||||
_loadModelName();
|
||||
final persisted = await _persistSettings({'api_base_url': _api.baseUrl});
|
||||
await _loadModelName();
|
||||
return persisted;
|
||||
}
|
||||
}
|
||||
|
||||
+113
-2
@@ -2,6 +2,67 @@ enum ModelConnectionStatus { checking, connected, unavailable }
|
||||
|
||||
enum RequestStatus { idle, generating, completed, cancelled, failed }
|
||||
|
||||
class ModelDescriptor {
|
||||
const ModelDescriptor({
|
||||
required this.id,
|
||||
required this.capabilities,
|
||||
required this.capabilitiesVerified,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final List<String> capabilities;
|
||||
final bool capabilitiesVerified;
|
||||
}
|
||||
|
||||
class AgentSkillDescriptor {
|
||||
const AgentSkillDescriptor({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.description,
|
||||
this.allowedTools = const [],
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
final String description;
|
||||
final List<String> allowedTools;
|
||||
|
||||
factory AgentSkillDescriptor.fromJson(Map<String, dynamic> json) =>
|
||||
AgentSkillDescriptor(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String? ?? '',
|
||||
allowedTools:
|
||||
(json['allowed_tools'] as List<dynamic>? ?? const [])
|
||||
.cast<String>(),
|
||||
);
|
||||
}
|
||||
|
||||
class FileChangeProposal {
|
||||
const FileChangeProposal({
|
||||
required this.token,
|
||||
required this.path,
|
||||
required this.operation,
|
||||
required this.diff,
|
||||
required this.expiresInSeconds,
|
||||
});
|
||||
|
||||
final String token;
|
||||
final String path;
|
||||
final String operation;
|
||||
final String diff;
|
||||
final int expiresInSeconds;
|
||||
|
||||
factory FileChangeProposal.fromJson(Map<String, dynamic> json) =>
|
||||
FileChangeProposal(
|
||||
token: json['token'] as String,
|
||||
path: json['path'] as String,
|
||||
operation: json['operation'] as String,
|
||||
diff: json['diff'] as String,
|
||||
expiresInSeconds: json['expires_in_seconds'] as int? ?? 600,
|
||||
);
|
||||
}
|
||||
|
||||
class ChatMessage {
|
||||
const ChatMessage(
|
||||
this.text, {
|
||||
@@ -87,7 +148,14 @@ class ChatState {
|
||||
this.activeConversationId,
|
||||
this.isLoadingHistory = true,
|
||||
this.isSending = false,
|
||||
this.agentProgress,
|
||||
this.pendingFileChangeProposal,
|
||||
this.notificationsEnabled = false,
|
||||
this.isDarkMode = false,
|
||||
this.isWorkspaceMode = false,
|
||||
this.selectedWorkspacePath,
|
||||
this.availableWorkspaceFiles = const [],
|
||||
this.selectedWorkspaceFiles = const [],
|
||||
this.isWebSearchMode = false,
|
||||
this.error,
|
||||
this.modelName = 'جارٍ اكتشاف النموذج…',
|
||||
@@ -96,6 +164,8 @@ class ChatState {
|
||||
this.elapsed = Duration.zero,
|
||||
this.lastResponseDuration,
|
||||
this.availableModels = const [],
|
||||
this.availableSkills = const [],
|
||||
this.selectedSkillId,
|
||||
this.selectedModel,
|
||||
required this.apiBaseUrl,
|
||||
});
|
||||
@@ -105,7 +175,14 @@ class ChatState {
|
||||
final String? activeConversationId;
|
||||
final bool isLoadingHistory;
|
||||
final bool isSending;
|
||||
final String? agentProgress;
|
||||
final FileChangeProposal? pendingFileChangeProposal;
|
||||
final bool notificationsEnabled;
|
||||
final bool isDarkMode;
|
||||
final bool isWorkspaceMode;
|
||||
final String? selectedWorkspacePath;
|
||||
final List<String> availableWorkspaceFiles;
|
||||
final List<String> selectedWorkspaceFiles;
|
||||
final bool isWebSearchMode;
|
||||
final String? error;
|
||||
final String modelName;
|
||||
@@ -113,7 +190,9 @@ class ChatState {
|
||||
final RequestStatus requestStatus;
|
||||
final Duration elapsed;
|
||||
final Duration? lastResponseDuration;
|
||||
final List<String> availableModels;
|
||||
final List<ModelDescriptor> availableModels;
|
||||
final List<AgentSkillDescriptor> availableSkills;
|
||||
final String? selectedSkillId;
|
||||
final String? selectedModel;
|
||||
final String apiBaseUrl;
|
||||
|
||||
@@ -124,7 +203,17 @@ class ChatState {
|
||||
bool clearActiveConversation = false,
|
||||
bool? isLoadingHistory,
|
||||
bool? isSending,
|
||||
String? agentProgress,
|
||||
bool clearAgentProgress = false,
|
||||
FileChangeProposal? pendingFileChangeProposal,
|
||||
bool clearPendingFileChangeProposal = false,
|
||||
bool? notificationsEnabled,
|
||||
bool? isDarkMode,
|
||||
bool? isWorkspaceMode,
|
||||
String? selectedWorkspacePath,
|
||||
bool clearSelectedWorkspacePath = false,
|
||||
List<String>? availableWorkspaceFiles,
|
||||
List<String>? selectedWorkspaceFiles,
|
||||
bool? isWebSearchMode,
|
||||
String? error,
|
||||
bool clearError = false,
|
||||
@@ -134,7 +223,10 @@ class ChatState {
|
||||
Duration? elapsed,
|
||||
Duration? lastResponseDuration,
|
||||
bool clearLastResponseDuration = false,
|
||||
List<String>? availableModels,
|
||||
List<ModelDescriptor>? availableModels,
|
||||
List<AgentSkillDescriptor>? availableSkills,
|
||||
String? selectedSkillId,
|
||||
bool clearSelectedSkill = false,
|
||||
String? selectedModel,
|
||||
bool clearSelectedModel = false,
|
||||
String? apiBaseUrl,
|
||||
@@ -147,7 +239,23 @@ class ChatState {
|
||||
: (activeConversationId ?? this.activeConversationId),
|
||||
isLoadingHistory: isLoadingHistory ?? this.isLoadingHistory,
|
||||
isSending: isSending ?? this.isSending,
|
||||
agentProgress:
|
||||
clearAgentProgress ? null : (agentProgress ?? this.agentProgress),
|
||||
pendingFileChangeProposal:
|
||||
clearPendingFileChangeProposal
|
||||
? null
|
||||
: (pendingFileChangeProposal ?? this.pendingFileChangeProposal),
|
||||
notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled,
|
||||
isDarkMode: isDarkMode ?? this.isDarkMode,
|
||||
isWorkspaceMode: isWorkspaceMode ?? this.isWorkspaceMode,
|
||||
selectedWorkspacePath:
|
||||
clearSelectedWorkspacePath
|
||||
? null
|
||||
: (selectedWorkspacePath ?? this.selectedWorkspacePath),
|
||||
availableWorkspaceFiles:
|
||||
availableWorkspaceFiles ?? this.availableWorkspaceFiles,
|
||||
selectedWorkspaceFiles:
|
||||
selectedWorkspaceFiles ?? this.selectedWorkspaceFiles,
|
||||
isWebSearchMode: isWebSearchMode ?? this.isWebSearchMode,
|
||||
error: clearError ? null : (error ?? this.error),
|
||||
modelName: modelName ?? this.modelName,
|
||||
@@ -159,6 +267,9 @@ class ChatState {
|
||||
? null
|
||||
: (lastResponseDuration ?? this.lastResponseDuration),
|
||||
availableModels: availableModels ?? this.availableModels,
|
||||
availableSkills: availableSkills ?? this.availableSkills,
|
||||
selectedSkillId:
|
||||
clearSelectedSkill ? null : (selectedSkillId ?? this.selectedSkillId),
|
||||
selectedModel:
|
||||
clearSelectedModel ? null : (selectedModel ?? this.selectedModel),
|
||||
apiBaseUrl: apiBaseUrl ?? this.apiBaseUrl,
|
||||
|
||||
+832
-144
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user