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,103 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:record/record.dart';
import '../../../../core/network/api_repository.dart';
import 'voice_state.dart';
class VoiceCubit extends Cubit<VoiceState> {
VoiceCubit(this._api, this._recorder) : super(const VoiceState());
final ApiRepository _api;
final AudioRecorder _recorder;
StreamSubscription<Uint8List>? _recordingSubscription;
final List<int> _pcm = [];
Future<void> startRecording() async {
emit(state.copyWith(clearError: true, clearTranscript: true));
try {
if (!await _recorder.hasPermission()) {
throw Exception('لم يتم السماح باستخدام الميكروفون.');
}
_pcm.clear();
final stream = await _recorder.startStream(
const RecordConfig(
encoder: AudioEncoder.pcm16bits,
sampleRate: 16000,
numChannels: 1,
),
);
_recordingSubscription = stream.listen(
_pcm.addAll,
onError: (Object error) {
if (!isClosed) emit(state.copyWith(error: error.toString()));
},
);
emit(state.copyWith(status: VoiceStatus.recording));
} catch (error) {
emit(state.copyWith(status: VoiceStatus.idle, error: error.toString()));
}
}
Future<void> stopAndTranscribe() async {
try {
await _recorder.stop();
await _recordingSubscription?.cancel();
_recordingSubscription = null;
if (_pcm.isEmpty) throw Exception('لم يلتقط التسجيل أي صوت.');
if (_pcm.length > 25 * 1024 * 1024 - 44) {
throw Exception('التسجيل طويل جدًا؛ الحد الحالي 25 ميغابايت.');
}
emit(state.copyWith(status: VoiceStatus.transcribing, clearError: true));
final transcript = await _api.transcribe(
_toWav(Uint8List.fromList(_pcm)),
);
emit(
state.copyWith(
status: VoiceStatus.idle,
transcript: transcript,
clearError: true,
),
);
} catch (error) {
emit(state.copyWith(status: VoiceStatus.idle, error: error.toString()));
} finally {
_pcm.clear();
}
}
Uint8List _toWav(Uint8List pcm) {
final wav = Uint8List(44 + pcm.length);
final view = ByteData.sublistView(wav);
void writeText(int offset, String value) {
for (var i = 0; i < value.length; i++) {
wav[offset + i] = value.codeUnitAt(i);
}
}
writeText(0, 'RIFF');
view.setUint32(4, 36 + pcm.length, Endian.little);
writeText(8, 'WAVE');
writeText(12, 'fmt ');
view.setUint32(16, 16, Endian.little);
view.setUint16(20, 1, Endian.little);
view.setUint16(22, 1, Endian.little);
view.setUint32(24, 16000, Endian.little);
view.setUint32(28, 32000, Endian.little);
view.setUint16(32, 2, Endian.little);
view.setUint16(34, 16, Endian.little);
writeText(36, 'data');
view.setUint32(40, pcm.length, Endian.little);
wav.setRange(44, wav.length, pcm);
return wav;
}
@override
Future<void> close() {
_recordingSubscription?.cancel();
_recorder.dispose();
return super.close();
}
}
@@ -0,0 +1,25 @@
enum VoiceStatus { idle, recording, transcribing }
class VoiceState {
const VoiceState({
this.status = VoiceStatus.idle,
this.transcript,
this.error,
});
final VoiceStatus status;
final String? transcript;
final String? error;
VoiceState copyWith({
VoiceStatus? status,
String? transcript,
String? error,
bool clearTranscript = false,
bool clearError = false,
}) => VoiceState(
status: status ?? this.status,
transcript: clearTranscript ? null : (transcript ?? this.transcript),
error: clearError ? null : (error ?? this.error),
);
}
@@ -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,
);
}