chore: establish Sovereign AI reference

This commit is contained in:
Hamza Ayed
2026-09-29 22:38:37 +03:00
commit ce1b22f100
147 changed files with 7394 additions and 0 deletions
@@ -0,0 +1,176 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/network/api_repository.dart';
import 'chat_state.dart';
class ChatCubit extends Cubit<ChatState> {
ChatCubit(this._api) : super(ChatState(apiBaseUrl: _api.baseUrl)) {
_loadHistory();
}
final ApiRepository _api;
Future<void> _loadHistory() async {
try {
final conversations = await _api.listConversations();
if (!isClosed) {
emit(
state.copyWith(
conversations: conversations,
isLoadingHistory: false,
clearError: true,
),
);
}
} catch (error) {
if (!isClosed) {
emit(
state.copyWith(
isLoadingHistory: false,
error: 'تعذر تحميل المحادثات من FastAPI/SQLite: $error',
),
);
}
}
}
Future<void> _saveCurrent(List<ChatMessage> messages) async {
if (messages.isEmpty) return;
final firstQuestion = messages.firstWhere(
(message) => message.fromUser,
orElse: () => const ChatMessage('محادثة جديدة', fromUser: true),
);
final id = state.activeConversationId ?? _api.newId();
final title =
firstQuestion.text.length > 42
? '${firstQuestion.text.substring(0, 42)}…'
: firstQuestion.text;
final item = SavedConversation(
id: id,
title: title,
updatedAt: DateTime.now(),
messages: List.unmodifiable(messages),
);
final conversations = [
item,
...state.conversations.where((conversation) => conversation.id != id),
];
emit(
state.copyWith(
messages: messages,
conversations: conversations,
activeConversationId: id,
),
);
await _api.saveConversation(item);
}
Future<void> send(String text) async {
final prompt = text.trim();
if (prompt.isEmpty || state.isSending || state.isLoadingHistory) return;
final messages = [...state.messages, ChatMessage(prompt, fromUser: true)];
var answer = '';
emit(state.copyWith(messages: messages, isSending: true, clearError: true));
try {
await _saveCurrent(messages);
if (isClosed) return;
emit(
state.copyWith(
messages: [...messages, const ChatMessage('', fromUser: false)],
),
);
await for (final delta in _api.completeStream(messages)) {
if (isClosed) return;
answer += delta;
emit(
state.copyWith(
messages: [...messages, ChatMessage(answer, fromUser: false)],
),
);
}
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);
} catch (error) {
if (isClosed) return;
final partial =
answer.isEmpty
? messages
: [...messages, ChatMessage(answer, fromUser: false)];
emit(
state.copyWith(
messages: partial,
isSending: false,
error: error.toString(),
),
);
if (answer.isNotEmpty) {
try {
await _saveCurrent(partial);
} catch (_) {
// The partial answer remains visible even if SQLite is unavailable.
}
}
}
}
Future<void> openConversation(SavedConversation conversation) async {
if (state.isSending) return;
try {
final fullConversation = await _api.getConversation(conversation.id);
emit(
state.copyWith(
messages: fullConversation.messages,
activeConversationId: fullConversation.id,
clearError: true,
),
);
} catch (error) {
emit(state.copyWith(error: error.toString()));
}
}
void newConversation() {
if (state.isSending) return;
emit(
state.copyWith(
messages: const [],
clearActiveConversation: true,
clearError: true,
),
);
}
Future<void> deleteConversation(String id) async {
if (state.isSending) return;
try {
await _api.deleteConversation(id);
final conversations =
state.conversations
.where((conversation) => conversation.id != id)
.toList();
if (state.activeConversationId == id) {
emit(
state.copyWith(
conversations: conversations,
messages: const [],
clearActiveConversation: true,
),
);
} else {
emit(state.copyWith(conversations: conversations));
}
} catch (error) {
emit(state.copyWith(error: error.toString()));
}
}
void setApiBaseUrl(String value) {
final url = value.trim();
if (url.isEmpty) return;
_api.setBaseUrl(url);
emit(state.copyWith(apiBaseUrl: _api.baseUrl, clearError: true));
}
}
@@ -0,0 +1,96 @@
class ChatMessage {
const ChatMessage(this.text, {required this.fromUser});
final String text;
final bool fromUser;
Map<String, dynamic> toJson() => {'text': text, 'fromUser': fromUser};
factory ChatMessage.fromJson(Map<String, dynamic> json) =>
ChatMessage(json['text'] as String, fromUser: json['fromUser'] as bool);
}
class SavedConversation {
const SavedConversation({
required this.id,
required this.title,
required this.updatedAt,
required this.messages,
});
final String id;
final String title;
final DateTime updatedAt;
final List<ChatMessage> messages;
Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'updatedAt': updatedAt.toIso8601String(),
'messages': messages.map((message) => message.toJson()).toList(),
};
factory SavedConversation.fromJson(Map<String, dynamic> json) =>
SavedConversation(
id: json['id'] as String,
title: json['title'] as String,
updatedAt: DateTime.parse(json['updatedAt'] as String),
messages:
(json['messages'] as List<dynamic>)
.map(
(item) => ChatMessage.fromJson(item as Map<String, dynamic>),
)
.toList(),
);
factory SavedConversation.fromSummaryJson(Map<String, dynamic> json) =>
SavedConversation(
id: json['id'] as String,
title: json['title'] as String,
updatedAt: DateTime.parse(json['updated_at'] as String).toLocal(),
messages: const [],
);
}
class ChatState {
const ChatState({
this.messages = const [],
this.conversations = const [],
this.activeConversationId,
this.isLoadingHistory = true,
this.isSending = false,
this.error,
required this.apiBaseUrl,
});
final List<ChatMessage> messages;
final List<SavedConversation> conversations;
final String? activeConversationId;
final bool isLoadingHistory;
final bool isSending;
final String? error;
final String apiBaseUrl;
ChatState copyWith({
List<ChatMessage>? messages,
List<SavedConversation>? conversations,
String? activeConversationId,
bool clearActiveConversation = false,
bool? isLoadingHistory,
bool? isSending,
String? error,
bool clearError = false,
String? apiBaseUrl,
}) => ChatState(
messages: messages ?? this.messages,
conversations: conversations ?? this.conversations,
activeConversationId:
clearActiveConversation
? null
: (activeConversationId ?? this.activeConversationId),
isLoadingHistory: isLoadingHistory ?? this.isLoadingHistory,
isSending: isSending ?? this.isSending,
error: clearError ? null : (error ?? this.error),
apiBaseUrl: apiBaseUrl ?? this.apiBaseUrl,
);
}