chore: establish Sovereign AI reference
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../../features/chat/presentation/cubit/chat_state.dart';
|
||||
|
||||
const _uuid = Uuid();
|
||||
|
||||
class ApiRepository {
|
||||
ApiRepository({required String baseUrl}) : _baseUrl = baseUrl;
|
||||
|
||||
String _baseUrl;
|
||||
String? _cachedUserId;
|
||||
Future<String>? _loadingUserId;
|
||||
String get baseUrl => _baseUrl;
|
||||
|
||||
String newId() => _uuid.v4();
|
||||
|
||||
void setBaseUrl(String value) =>
|
||||
_baseUrl = value.replaceFirst(RegExp(r'/+$'), '');
|
||||
|
||||
Future<String> _userId() async {
|
||||
final cached = _cachedUserId;
|
||||
if (cached != null) return cached;
|
||||
return _loadingUserId ??= _loadLocalUserId();
|
||||
}
|
||||
|
||||
Future<String> _loadLocalUserId() async {
|
||||
final response = await http.get(Uri.parse('$_baseUrl/v1/local-user'));
|
||||
_checkStatus(response);
|
||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
_cachedUserId = data['user_id'] as String;
|
||||
return _cachedUserId!;
|
||||
}
|
||||
|
||||
Future<Map<String, String>> _userHeaders() async => {
|
||||
'Content-Type': 'application/json',
|
||||
'X-User-ID': await _userId(),
|
||||
};
|
||||
|
||||
void _checkStatus(http.Response response) {
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
throw Exception('API ${response.statusCode}: ${response.body}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<SavedConversation>> listConversations() async {
|
||||
final response = await http.get(
|
||||
Uri.parse('$_baseUrl/v1/conversations'),
|
||||
headers: await _userHeaders(),
|
||||
);
|
||||
_checkStatus(response);
|
||||
return (jsonDecode(response.body) as List<dynamic>)
|
||||
.map(
|
||||
(item) =>
|
||||
SavedConversation.fromSummaryJson(item as Map<String, dynamic>),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<SavedConversation> getConversation(String id) async {
|
||||
final response = await http.get(
|
||||
Uri.parse('$_baseUrl/v1/conversations/$id'),
|
||||
headers: await _userHeaders(),
|
||||
);
|
||||
_checkStatus(response);
|
||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
return SavedConversation(
|
||||
id: data['id'] as String,
|
||||
title: data['title'] as String,
|
||||
updatedAt: DateTime.parse(data['updated_at'] as String).toLocal(),
|
||||
messages:
|
||||
(data['messages'] as List<dynamic>).map((item) {
|
||||
final message = item as Map<String, dynamic>;
|
||||
return ChatMessage(
|
||||
message['content'] as String,
|
||||
fromUser: message['role'] == 'user',
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> saveConversation(SavedConversation conversation) async {
|
||||
final response = await http.put(
|
||||
Uri.parse('$_baseUrl/v1/conversations/${conversation.id}'),
|
||||
headers: await _userHeaders(),
|
||||
body: jsonEncode({
|
||||
'title': conversation.title,
|
||||
'messages':
|
||||
conversation.messages
|
||||
.map(
|
||||
(message) => {
|
||||
'role': message.fromUser ? 'user' : 'assistant',
|
||||
'content': message.text,
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
}),
|
||||
);
|
||||
_checkStatus(response);
|
||||
}
|
||||
|
||||
Future<void> deleteConversation(String id) async {
|
||||
final response = await http.delete(
|
||||
Uri.parse('$_baseUrl/v1/conversations/$id'),
|
||||
headers: await _userHeaders(),
|
||||
);
|
||||
_checkStatus(response);
|
||||
}
|
||||
|
||||
Future<String> complete(List<ChatMessage> messages) async {
|
||||
final response = await http
|
||||
.post(
|
||||
Uri.parse('$_baseUrl/v1/chat/completions'),
|
||||
headers: const {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({
|
||||
'messages':
|
||||
messages
|
||||
.map(
|
||||
(message) => {
|
||||
'role': message.fromUser ? 'user' : 'assistant',
|
||||
'content': message.text,
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
}),
|
||||
)
|
||||
.timeout(const Duration(minutes: 3));
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
throw Exception('API ${response.statusCode}: ${response.body}');
|
||||
}
|
||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final choices = data['choices'] as List<dynamic>;
|
||||
final message = choices.first['message'] as Map<String, dynamic>;
|
||||
return message['content'] as String? ?? 'لم يصل نص في الإجابة.';
|
||||
}
|
||||
|
||||
Stream<String> completeStream(List<ChatMessage> messages) async* {
|
||||
final client = http.Client();
|
||||
try {
|
||||
final request =
|
||||
http.Request('POST', Uri.parse('$_baseUrl/v1/chat/stream'))
|
||||
..headers.addAll({
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/x-ndjson',
|
||||
})
|
||||
..body = jsonEncode({
|
||||
'messages':
|
||||
messages
|
||||
.map(
|
||||
(message) => {
|
||||
'role': message.fromUser ? 'user' : 'assistant',
|
||||
'content': message.text,
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
});
|
||||
final response = await client
|
||||
.send(request)
|
||||
.timeout(const Duration(minutes: 3));
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
final body = await response.stream.bytesToString();
|
||||
throw Exception('API ${response.statusCode}: $body');
|
||||
}
|
||||
var completed = false;
|
||||
await for (final line in response.stream
|
||||
.transform(utf8.decoder)
|
||||
.transform(const LineSplitter())
|
||||
.timeout(const Duration(minutes: 3))) {
|
||||
if (line.isEmpty) continue;
|
||||
final event = jsonDecode(line) as Map<String, dynamic>;
|
||||
if (event['error'] != null) {
|
||||
throw Exception(event['error']);
|
||||
}
|
||||
if (event['done'] == true) {
|
||||
completed = true;
|
||||
break;
|
||||
}
|
||||
final delta = event['delta'] as String?;
|
||||
if (delta != null && delta.isNotEmpty) yield delta;
|
||||
}
|
||||
if (!completed) throw Exception('انقطع بث الإجابة قبل اكتماله.');
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> transcribe(Uint8List wavBytes) async {
|
||||
final request = http.MultipartRequest(
|
||||
'POST',
|
||||
Uri.parse('$_baseUrl/v1/audio/transcriptions'),
|
||||
)
|
||||
..files.add(
|
||||
http.MultipartFile.fromBytes(
|
||||
'file',
|
||||
wavBytes,
|
||||
filename: 'recording.wav',
|
||||
contentType: MediaType('audio', 'wav'),
|
||||
),
|
||||
);
|
||||
final streamed = await request.send().timeout(const Duration(minutes: 3));
|
||||
final response = await http.Response.fromStream(streamed);
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
throw Exception(
|
||||
'Transcription API ${response.statusCode}: ${response.body}',
|
||||
);
|
||||
}
|
||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
return data['text'] as String? ?? '';
|
||||
}
|
||||
}
|
||||
+103
@@ -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();
|
||||
}
|
||||
}
|
||||
+25
@@ -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,
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:record/record.dart';
|
||||
|
||||
import 'core/network/api_repository.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';
|
||||
|
||||
const _green = Color(0xFF167C68);
|
||||
const _paper = Color(0xFFF6F5F0);
|
||||
|
||||
void main() {
|
||||
final api = ApiRepository(
|
||||
baseUrl: const String.fromEnvironment(
|
||||
'API_BASE_URL',
|
||||
defaultValue: 'http://127.0.0.1:8000',
|
||||
),
|
||||
);
|
||||
runApp(
|
||||
MultiRepositoryProvider(
|
||||
providers: [RepositoryProvider.value(value: api)],
|
||||
child: MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(create: (_) => ChatCubit(api)),
|
||||
BlocProvider(create: (_) => VoiceCubit(api, AudioRecorder())),
|
||||
],
|
||||
child: const SovereignApp(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class SovereignApp extends StatelessWidget {
|
||||
const SovereignApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => MaterialApp(
|
||||
title: 'Sovereign AI',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
scaffoldBackgroundColor: _paper,
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: _green, surface: _paper),
|
||||
fontFamily: 'Segoe UI',
|
||||
),
|
||||
home: const ChatPage(),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user