refactor: 重构聊天数据层至core并简化首页UI
This commit is contained in:
@@ -18,7 +18,9 @@ import '../../features/auth/data/repositories/auth_repository_impl.dart';
|
||||
import '../../features/auth/presentation/bloc/auth_bloc.dart';
|
||||
import '../../features/auth/presentation/bloc/auth_event.dart';
|
||||
import '../../features/chat/presentation/bloc/chat_bloc.dart';
|
||||
import '../../features/chat/data/repositories/chat_history_repository.dart';
|
||||
import '../../core/chat/chat_api.dart';
|
||||
import '../../core/chat/chat_history_repository.dart';
|
||||
import '../../features/chat/data/apis/chat_api_impl.dart';
|
||||
import '../../features/calendar/data/apis/calendar_api.dart';
|
||||
import '../../features/calendar/data/services/calendar_service.dart';
|
||||
import '../../shared/state/calendar_state_manager.dart';
|
||||
@@ -127,8 +129,11 @@ Future<void> configureDependencies() async {
|
||||
InboxRepositoryImpl(apiClient: apiClient, store: hybridCacheStore),
|
||||
);
|
||||
|
||||
final chatApi = ChatApiImpl(apiClient);
|
||||
sl.registerSingleton<ChatApi>(chatApi);
|
||||
|
||||
final chatHistoryRepository = ChatHistoryRepository(
|
||||
apiClient: apiClient,
|
||||
chatApi: chatApi,
|
||||
store: hybridCacheStore,
|
||||
);
|
||||
sl.registerSingleton<ChatHistoryRepository>(chatHistoryRepository);
|
||||
@@ -167,7 +172,7 @@ Future<void> configureDependencies() async {
|
||||
sl.registerSingleton<AuthBloc>(authBloc);
|
||||
sl.registerSingleton<SessionController>(AuthSessionController(authBloc));
|
||||
sl.registerSingleton<ChatBloc>(
|
||||
ChatBloc(apiClient: apiClient, historyRepository: chatHistoryRepository),
|
||||
ChatBloc(chatApi: chatApi, historyRepository: chatHistoryRepository),
|
||||
);
|
||||
|
||||
apiClient.setRefreshCallback((token) async {
|
||||
|
||||
@@ -73,21 +73,12 @@ String? resolveAuthRedirect({
|
||||
final isProtected =
|
||||
isHomeRoute ||
|
||||
_protectedRoutes.any((route) => matchedLocation.startsWith(route));
|
||||
final prewarmStatus = prewarm?.status ?? AppPrewarmStatus.completed;
|
||||
final shouldBlockForPrewarm =
|
||||
isAuthenticated && prewarmStatus == AppPrewarmStatus.running;
|
||||
|
||||
if (shouldBlockForPrewarm && !isBootRoute) {
|
||||
return AppRoutes.authBoot;
|
||||
}
|
||||
final _ = prewarm;
|
||||
|
||||
if (isAuthChecking && !isBootRoute) {
|
||||
return AppRoutes.authBoot;
|
||||
}
|
||||
if (!isAuthChecking && isBootRoute) {
|
||||
if (shouldBlockForPrewarm) {
|
||||
return null;
|
||||
}
|
||||
return isAuthenticated ? AppRoutes.homeMain : AppRoutes.authLogin;
|
||||
}
|
||||
if (!isAuthenticated && isProtected) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../features/calendar/data/repositories/calendar_repository.dart';
|
||||
import '../../features/messages/data/repositories/inbox_repository.dart';
|
||||
import '../../features/chat/data/repositories/chat_history_repository.dart';
|
||||
import '../../core/chat/chat_history_repository.dart';
|
||||
|
||||
enum AppPrewarmStatus { idle, running, completed, timedOut, failed }
|
||||
|
||||
|
||||
+55
-117
@@ -3,17 +3,28 @@ import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:social_app/data/network/i_api_client.dart';
|
||||
|
||||
import '../models/ag_ui_event.dart';
|
||||
import '../repositories/chat_history_repository.dart';
|
||||
import 'chat_api.dart';
|
||||
import 'ag_ui_event.dart';
|
||||
import 'chat_history_repository.dart';
|
||||
|
||||
typedef EventCallback = void Function(AgUiEvent event);
|
||||
|
||||
const _runIdPrefix = 'run_';
|
||||
|
||||
class AttachmentUploadInput {
|
||||
const AttachmentUploadInput({
|
||||
required this.name,
|
||||
required this.mimeType,
|
||||
required this.bytes,
|
||||
required this.localPath,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String mimeType;
|
||||
final Uint8List bytes;
|
||||
final String localPath;
|
||||
}
|
||||
|
||||
class UploadedAttachment {
|
||||
const UploadedAttachment({
|
||||
required this.localPath,
|
||||
@@ -43,7 +54,7 @@ class _RunInputPayload {
|
||||
}
|
||||
|
||||
class AgUiService {
|
||||
final IApiClient _apiClient;
|
||||
final ChatApi _chatApi;
|
||||
final ChatHistoryRepository? _historyRepository;
|
||||
EventCallback onEvent;
|
||||
final Map<String, String> _lastEventIdByThread = {};
|
||||
@@ -58,30 +69,23 @@ class AgUiService {
|
||||
|
||||
AgUiService({
|
||||
EventCallback? onEvent,
|
||||
required IApiClient apiClient,
|
||||
required ChatApi chatApi,
|
||||
ChatHistoryRepository? historyRepository,
|
||||
}) : onEvent = onEvent ?? ((_) {}),
|
||||
_apiClient = apiClient,
|
||||
_chatApi = chatApi,
|
||||
_historyRepository = historyRepository;
|
||||
|
||||
Future<SendMessageResult> sendMessage(
|
||||
String content, {
|
||||
List<XFile>? images,
|
||||
List<AttachmentUploadInput>? attachments,
|
||||
}) async {
|
||||
await _cancelActiveSseSubscription();
|
||||
final streamToken = ++_activeStreamToken;
|
||||
final runInputPayload = await _buildRunInput(
|
||||
content: content,
|
||||
images: images,
|
||||
attachments: attachments,
|
||||
);
|
||||
final response = await _apiClient.post<Map<String, dynamic>>(
|
||||
'/api/v1/agent/runs',
|
||||
data: runInputPayload.input,
|
||||
);
|
||||
final payload = response.data;
|
||||
if (payload is! Map<String, dynamic>) {
|
||||
throw StateError('Invalid /agent/runs response');
|
||||
}
|
||||
final payload = await _chatApi.createRun(runInputPayload.input);
|
||||
final threadId = payload['threadId'] as String?;
|
||||
final runId = payload['runId'] as String?;
|
||||
if (threadId == null || threadId.isEmpty) {
|
||||
@@ -126,49 +130,18 @@ class AgUiService {
|
||||
}
|
||||
|
||||
Future<HistorySnapshot> _loadHistoryFromApi({DateTime? beforeDate}) async {
|
||||
final path = _buildHistoryPath(beforeDate: beforeDate);
|
||||
final response = await _apiClient.get<Map<String, dynamic>>(path);
|
||||
final payload = response.data;
|
||||
if (payload is! Map<String, dynamic>) {
|
||||
throw StateError('Invalid /agent/history response');
|
||||
}
|
||||
final payload = await _chatApi.fetchHistory(
|
||||
threadId: _threadId,
|
||||
beforeDate: beforeDate,
|
||||
);
|
||||
return HistorySnapshot.fromJson(payload);
|
||||
}
|
||||
|
||||
Future<Uint8List> fetchAttachmentPreview(String previewPath) async {
|
||||
final response = await _apiClient.get<List<int>>(
|
||||
previewPath,
|
||||
options: Options(responseType: ResponseType.bytes),
|
||||
);
|
||||
final payload = response.data;
|
||||
if (payload is List<int>) {
|
||||
return Uint8List.fromList(payload);
|
||||
}
|
||||
throw StateError('Invalid attachment payload');
|
||||
}
|
||||
Future<Uint8List> fetchAttachmentPreview(String previewPath) =>
|
||||
_chatApi.fetchAttachmentPreview(previewPath);
|
||||
|
||||
Future<String> transcribeAudio(String filePath) async {
|
||||
final formData = FormData.fromMap({
|
||||
'audio': await MultipartFile.fromFile(
|
||||
filePath,
|
||||
filename: 'recording.wav',
|
||||
contentType: DioMediaType('audio', 'wav'),
|
||||
),
|
||||
});
|
||||
final response = await _apiClient.post<Map<String, dynamic>>(
|
||||
'/api/v1/agent/transcribe',
|
||||
data: formData,
|
||||
);
|
||||
final payload = response.data;
|
||||
if (payload is! Map<String, dynamic>) {
|
||||
throw StateError('Invalid /agent/transcribe response');
|
||||
}
|
||||
final transcript = payload['transcript'];
|
||||
if (transcript is! String) {
|
||||
throw StateError('Missing transcript in /agent/transcribe response');
|
||||
}
|
||||
return transcript;
|
||||
}
|
||||
Future<String> transcribeAudio(String filePath) =>
|
||||
_chatApi.transcribeAudio(filePath);
|
||||
|
||||
bool hasEarlierHistory(DateTime fromDate) {
|
||||
// Whether earlier history exists is driven by backend snapshot.hasMore.
|
||||
@@ -181,10 +154,7 @@ class AgUiService {
|
||||
final activeThreadId = _activeThreadIdForRun;
|
||||
final activeRunId = _activeRunId;
|
||||
if (activeThreadId != null && activeRunId != null) {
|
||||
final encodedRunId = Uri.encodeQueryComponent(activeRunId);
|
||||
await _apiClient.post<Map<String, dynamic>>(
|
||||
'/api/v1/agent/runs/$activeThreadId/cancel?runId=$encodedRunId',
|
||||
);
|
||||
await _chatApi.cancelRun(threadId: activeThreadId, runId: activeRunId);
|
||||
_activeThreadIdForRun = null;
|
||||
_activeRunId = null;
|
||||
_activeStreamToken += 1;
|
||||
@@ -214,14 +184,9 @@ class AgUiService {
|
||||
required String expectedRunId,
|
||||
required int streamToken,
|
||||
}) async {
|
||||
final lastEventId = _lastEventIdByThread[threadId];
|
||||
final headers = <String, String>{'Accept': 'text/event-stream'};
|
||||
if (lastEventId != null && lastEventId.isNotEmpty) {
|
||||
headers['Last-Event-ID'] = lastEventId;
|
||||
}
|
||||
final sseLines = await _apiClient.getSseLines(
|
||||
'/api/v1/agent/runs/$threadId/events',
|
||||
headers: headers,
|
||||
final sseLines = await _chatApi.streamRunEvents(
|
||||
threadId,
|
||||
lastEventId: _lastEventIdByThread[threadId],
|
||||
);
|
||||
|
||||
String? eventType;
|
||||
@@ -369,7 +334,7 @@ class AgUiService {
|
||||
|
||||
Future<_RunInputPayload> _buildRunInput({
|
||||
required String content,
|
||||
List<XFile>? images,
|
||||
List<AttachmentUploadInput>? attachments,
|
||||
}) async {
|
||||
final threadId = _threadId ?? _newUuid();
|
||||
final runId = _nextId(_runIdPrefix);
|
||||
@@ -381,10 +346,10 @@ class AgUiService {
|
||||
}
|
||||
|
||||
var uploadedAttachments = const <UploadedAttachment>[];
|
||||
if (images != null && images.isNotEmpty) {
|
||||
if (attachments != null && attachments.isNotEmpty) {
|
||||
uploadedAttachments = await _uploadAttachments(
|
||||
threadId: threadId,
|
||||
images: images,
|
||||
attachments: attachments,
|
||||
);
|
||||
for (final attachment in uploadedAttachments) {
|
||||
contentBlocks.add({
|
||||
@@ -423,36 +388,24 @@ class AgUiService {
|
||||
|
||||
Future<List<UploadedAttachment>> _uploadAttachments({
|
||||
required String threadId,
|
||||
required List<XFile> images,
|
||||
required List<AttachmentUploadInput> attachments,
|
||||
}) async {
|
||||
final attachments = <UploadedAttachment>[];
|
||||
for (final image in images) {
|
||||
final mimeType = image.mimeType ?? 'image/jpeg';
|
||||
final fileBytes = await image.readAsBytes();
|
||||
final formData = FormData.fromMap({
|
||||
'threadId': threadId,
|
||||
'file': MultipartFile.fromBytes(
|
||||
fileBytes,
|
||||
filename: image.name,
|
||||
contentType: DioMediaType.parse(mimeType),
|
||||
),
|
||||
});
|
||||
final response = await _apiClient.post<Map<String, dynamic>>(
|
||||
'/api/v1/agent/attachments',
|
||||
data: formData,
|
||||
final uploaded = <UploadedAttachment>[];
|
||||
for (final attachment in attachments) {
|
||||
final payload = await _chatApi.uploadAttachment(
|
||||
threadId: threadId,
|
||||
filename: attachment.name,
|
||||
mimeType: attachment.mimeType,
|
||||
bytes: attachment.bytes,
|
||||
);
|
||||
final payload = response.data;
|
||||
if (payload is! Map<String, dynamic>) {
|
||||
throw StateError('Invalid /agent/attachments response');
|
||||
}
|
||||
final attachment = payload['attachment'];
|
||||
if (attachment is! Map<String, dynamic>) {
|
||||
final payloadAttachment = payload['attachment'];
|
||||
if (payloadAttachment is! Map<String, dynamic>) {
|
||||
throw StateError('Missing attachment in /agent/attachments response');
|
||||
}
|
||||
final bucket = attachment['bucket'];
|
||||
final path = attachment['path'];
|
||||
final uploadedMime = attachment['mimeType'];
|
||||
final url = attachment['url'];
|
||||
final bucket = payloadAttachment['bucket'];
|
||||
final path = payloadAttachment['path'];
|
||||
final uploadedMime = payloadAttachment['mimeType'];
|
||||
final url = payloadAttachment['url'];
|
||||
if (bucket is! String ||
|
||||
path is! String ||
|
||||
uploadedMime is! String ||
|
||||
@@ -463,30 +416,15 @@ class AgUiService {
|
||||
url.isEmpty) {
|
||||
throw StateError('Invalid attachment reference');
|
||||
}
|
||||
attachments.add(
|
||||
uploaded.add(
|
||||
UploadedAttachment(
|
||||
localPath: image.path,
|
||||
localPath: attachment.localPath,
|
||||
url: url,
|
||||
mimeType: uploadedMime,
|
||||
),
|
||||
);
|
||||
}
|
||||
return attachments;
|
||||
}
|
||||
|
||||
String _buildHistoryPath({DateTime? beforeDate}) {
|
||||
final query = <String>[];
|
||||
if (_threadId != null && _threadId!.isNotEmpty) {
|
||||
query.add('threadId=$_threadId');
|
||||
}
|
||||
if (beforeDate != null) {
|
||||
final day = DateTime(beforeDate.year, beforeDate.month, beforeDate.day);
|
||||
query.add('before=${day.toIso8601String().substring(0, 10)}');
|
||||
}
|
||||
if (query.isEmpty) {
|
||||
return '/api/v1/agent/history';
|
||||
}
|
||||
return '/api/v1/agent/history?${query.join('&')}';
|
||||
return uploaded;
|
||||
}
|
||||
|
||||
String _nextId(String prefix) =>
|
||||
@@ -0,0 +1,28 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
abstract class ChatApi {
|
||||
Future<Map<String, dynamic>> createRun(Map<String, dynamic> runInput);
|
||||
|
||||
Future<Stream<String>> streamRunEvents(
|
||||
String threadId, {
|
||||
String? lastEventId,
|
||||
});
|
||||
|
||||
Future<Map<String, dynamic>> fetchHistory({
|
||||
String? threadId,
|
||||
DateTime? beforeDate,
|
||||
});
|
||||
|
||||
Future<Map<String, dynamic>> uploadAttachment({
|
||||
required String threadId,
|
||||
required String filename,
|
||||
required String mimeType,
|
||||
required Uint8List bytes,
|
||||
});
|
||||
|
||||
Future<Uint8List> fetchAttachmentPreview(String previewPath);
|
||||
|
||||
Future<String> transcribeAudio(String filePath);
|
||||
|
||||
Future<void> cancelRun({required String threadId, required String runId});
|
||||
}
|
||||
+9
-26
@@ -1,12 +1,12 @@
|
||||
import 'package:social_app/data/network/i_api_client.dart';
|
||||
import 'chat_api.dart';
|
||||
import 'package:social_app/data/cache/cache_policy.dart';
|
||||
import 'package:social_app/data/cache/cached_repository.dart';
|
||||
|
||||
import '../models/ag_ui_event.dart';
|
||||
import 'ag_ui_event.dart';
|
||||
|
||||
class ChatHistoryRepository extends CachedRepository<HistorySnapshot> {
|
||||
ChatHistoryRepository({required IApiClient apiClient, required super.store})
|
||||
: _apiClient = apiClient,
|
||||
ChatHistoryRepository({required ChatApi chatApi, required super.store})
|
||||
: _chatApi = chatApi,
|
||||
super(
|
||||
policy: const CachePolicy(
|
||||
softTtl: Duration(seconds: 30),
|
||||
@@ -17,7 +17,7 @@ class ChatHistoryRepository extends CachedRepository<HistorySnapshot> {
|
||||
decodeValue: _decodeSnapshot,
|
||||
);
|
||||
|
||||
final IApiClient _apiClient;
|
||||
final ChatApi _chatApi;
|
||||
|
||||
Future<HistorySnapshot> loadHistory({
|
||||
String? threadId,
|
||||
@@ -37,30 +37,13 @@ class ChatHistoryRepository extends CachedRepository<HistorySnapshot> {
|
||||
String? threadId,
|
||||
DateTime? beforeDate,
|
||||
}) async {
|
||||
final path = _buildHistoryPath(threadId: threadId, beforeDate: beforeDate);
|
||||
final response = await _apiClient.get<Map<String, dynamic>>(path);
|
||||
final payload = response.data;
|
||||
if (payload is! Map<String, dynamic>) {
|
||||
throw StateError('Invalid /agent/history response');
|
||||
}
|
||||
final payload = await _chatApi.fetchHistory(
|
||||
threadId: threadId,
|
||||
beforeDate: beforeDate,
|
||||
);
|
||||
return HistorySnapshot.fromJson(payload);
|
||||
}
|
||||
|
||||
static String _buildHistoryPath({String? threadId, DateTime? beforeDate}) {
|
||||
final query = <String>[];
|
||||
if (threadId != null && threadId.isNotEmpty) {
|
||||
query.add('threadId=$threadId');
|
||||
}
|
||||
if (beforeDate != null) {
|
||||
final day = DateTime(beforeDate.year, beforeDate.month, beforeDate.day);
|
||||
query.add('before=${day.toIso8601String().substring(0, 10)}');
|
||||
}
|
||||
if (query.isEmpty) {
|
||||
return '/api/v1/agent/history';
|
||||
}
|
||||
return '/api/v1/agent/history?${query.join('&')}';
|
||||
}
|
||||
|
||||
static String _keyFor({String? threadId, DateTime? beforeDate}) {
|
||||
final threadPart = (threadId == null || threadId.isEmpty)
|
||||
? 'default'
|
||||
@@ -0,0 +1,142 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:social_app/core/chat/chat_api.dart';
|
||||
import 'package:social_app/data/network/i_api_client.dart';
|
||||
|
||||
class ChatApiImpl implements ChatApi {
|
||||
ChatApiImpl(this._apiClient);
|
||||
|
||||
final IApiClient _apiClient;
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> createRun(Map<String, dynamic> runInput) async {
|
||||
final response = await _apiClient.post<Map<String, dynamic>>(
|
||||
'/api/v1/agent/runs',
|
||||
data: runInput,
|
||||
);
|
||||
final payload = response.data;
|
||||
if (payload is! Map<String, dynamic>) {
|
||||
throw StateError('Invalid /agent/runs response');
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Stream<String>> streamRunEvents(
|
||||
String threadId, {
|
||||
String? lastEventId,
|
||||
}) {
|
||||
final headers = <String, String>{'Accept': 'text/event-stream'};
|
||||
if (lastEventId != null && lastEventId.isNotEmpty) {
|
||||
headers['Last-Event-ID'] = lastEventId;
|
||||
}
|
||||
return _apiClient.getSseLines(
|
||||
'/api/v1/agent/runs/$threadId/events',
|
||||
headers: headers,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> fetchHistory({
|
||||
String? threadId,
|
||||
DateTime? beforeDate,
|
||||
}) async {
|
||||
final path = _buildHistoryPath(threadId: threadId, beforeDate: beforeDate);
|
||||
final response = await _apiClient.get<Map<String, dynamic>>(path);
|
||||
final payload = response.data;
|
||||
if (payload is! Map<String, dynamic>) {
|
||||
throw StateError('Invalid /agent/history response');
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> uploadAttachment({
|
||||
required String threadId,
|
||||
required String filename,
|
||||
required String mimeType,
|
||||
required Uint8List bytes,
|
||||
}) async {
|
||||
final formData = FormData.fromMap({
|
||||
'threadId': threadId,
|
||||
'file': MultipartFile.fromBytes(
|
||||
bytes,
|
||||
filename: filename,
|
||||
contentType: DioMediaType.parse(mimeType),
|
||||
),
|
||||
});
|
||||
final response = await _apiClient.post<Map<String, dynamic>>(
|
||||
'/api/v1/agent/attachments',
|
||||
data: formData,
|
||||
);
|
||||
final payload = response.data;
|
||||
if (payload is! Map<String, dynamic>) {
|
||||
throw StateError('Invalid /agent/attachments response');
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Uint8List> fetchAttachmentPreview(String previewPath) async {
|
||||
final response = await _apiClient.get<List<int>>(
|
||||
previewPath,
|
||||
options: Options(responseType: ResponseType.bytes),
|
||||
);
|
||||
final payload = response.data;
|
||||
if (payload is! List<int>) {
|
||||
throw StateError('Invalid attachment payload');
|
||||
}
|
||||
return Uint8List.fromList(payload);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> transcribeAudio(String filePath) async {
|
||||
final formData = FormData.fromMap({
|
||||
'audio': await MultipartFile.fromFile(
|
||||
filePath,
|
||||
filename: 'recording.wav',
|
||||
contentType: DioMediaType('audio', 'wav'),
|
||||
),
|
||||
});
|
||||
final response = await _apiClient.post<Map<String, dynamic>>(
|
||||
'/api/v1/agent/transcribe',
|
||||
data: formData,
|
||||
);
|
||||
final payload = response.data;
|
||||
if (payload is! Map<String, dynamic>) {
|
||||
throw StateError('Invalid /agent/transcribe response');
|
||||
}
|
||||
final transcript = payload['transcript'];
|
||||
if (transcript is! String) {
|
||||
throw StateError('Missing transcript in /agent/transcribe response');
|
||||
}
|
||||
return transcript;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> cancelRun({
|
||||
required String threadId,
|
||||
required String runId,
|
||||
}) async {
|
||||
final encodedRunId = Uri.encodeQueryComponent(runId);
|
||||
await _apiClient.post<Map<String, dynamic>>(
|
||||
'/api/v1/agent/runs/$threadId/cancel?runId=$encodedRunId',
|
||||
);
|
||||
}
|
||||
|
||||
static String _buildHistoryPath({String? threadId, DateTime? beforeDate}) {
|
||||
final query = <String>[];
|
||||
if (threadId != null && threadId.isNotEmpty) {
|
||||
query.add('threadId=${Uri.encodeQueryComponent(threadId)}');
|
||||
}
|
||||
if (beforeDate != null) {
|
||||
final day = DateTime(beforeDate.year, beforeDate.month, beforeDate.day);
|
||||
query.add('before=${day.toIso8601String().substring(0, 10)}');
|
||||
}
|
||||
if (query.isEmpty) {
|
||||
return '/api/v1/agent/history';
|
||||
}
|
||||
return '/api/v1/agent/history?${query.join('&')}';
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import '../../../../core/l10n/l10n.dart';
|
||||
import '../../data/models/ag_ui_event.dart';
|
||||
import '../../../../core/chat/ag_ui_event.dart';
|
||||
|
||||
String agUiEventLabel(AgUiEventType type) {
|
||||
final l10n = L10n.current;
|
||||
|
||||
@@ -2,16 +2,15 @@ import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:social_app/core/chat/chat_api.dart';
|
||||
import 'package:social_app/core/chat/agent_stage.dart';
|
||||
import 'package:social_app/core/chat/ag_ui_event.dart';
|
||||
import 'package:social_app/core/chat/ag_ui_service.dart';
|
||||
import 'package:social_app/core/chat/chat_list_item.dart';
|
||||
import 'package:social_app/core/chat/chat_orchestrator.dart';
|
||||
import 'package:social_app/data/network/i_api_client.dart';
|
||||
import 'package:social_app/core/chat/chat_history_repository.dart';
|
||||
import 'package:social_app/core/l10n/l10n.dart';
|
||||
|
||||
import '../../data/models/ag_ui_event.dart';
|
||||
import '../../data/repositories/chat_history_repository.dart';
|
||||
import '../../data/services/ag_ui_service.dart';
|
||||
|
||||
class ChatState implements ChatOrchestratorState {
|
||||
@override
|
||||
final List<ChatListItem> items;
|
||||
@@ -98,14 +97,11 @@ class ChatState implements ChatOrchestratorState {
|
||||
class ChatBloc extends Cubit<ChatState> implements ChatOrchestrator {
|
||||
ChatBloc({
|
||||
AgUiService? service,
|
||||
required IApiClient apiClient,
|
||||
required ChatApi chatApi,
|
||||
ChatHistoryRepository? historyRepository,
|
||||
}) : _service =
|
||||
service ??
|
||||
AgUiService(
|
||||
apiClient: apiClient,
|
||||
historyRepository: historyRepository,
|
||||
),
|
||||
AgUiService(chatApi: chatApi, historyRepository: historyRepository),
|
||||
super(const ChatState()) {
|
||||
_service.onEvent = _handleEvent;
|
||||
}
|
||||
@@ -434,7 +430,20 @@ class ChatBloc extends Cubit<ChatState> implements ChatOrchestrator {
|
||||
),
|
||||
);
|
||||
try {
|
||||
final sendResult = await _service.sendMessage(content, images: images);
|
||||
final uploadInputs = await Future.wait(
|
||||
(images ?? const <XFile>[]).map(
|
||||
(image) async => AttachmentUploadInput(
|
||||
name: image.name,
|
||||
mimeType: image.mimeType ?? 'image/jpeg',
|
||||
bytes: await image.readAsBytes(),
|
||||
localPath: image.path,
|
||||
),
|
||||
),
|
||||
);
|
||||
final sendResult = await _service.sendMessage(
|
||||
content,
|
||||
attachments: uploadInputs,
|
||||
);
|
||||
_syncUploadedAttachments(
|
||||
messageId: messageId,
|
||||
uploadedAttachments: sendResult.uploadedAttachments,
|
||||
|
||||
@@ -393,12 +393,6 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
try {
|
||||
if (hasEarlierHistory) {
|
||||
await _loadMoreHistoryPreservingViewport(chatBloc);
|
||||
} else {
|
||||
Toast.show(
|
||||
context,
|
||||
context.l10n.homeNoEarlierHistory,
|
||||
type: ToastType.info,
|
||||
);
|
||||
}
|
||||
_applyViewportDecision(
|
||||
_dispatchViewportEvent(
|
||||
@@ -687,29 +681,6 @@ class _HomeEmptyStateAmbient extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Center(
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
key: homeEmptyStateAmbientKey,
|
||||
width: double.infinity,
|
||||
height: AppSpacing.xxl * 6,
|
||||
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.xxl),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(AppSpacing.xxl * 2),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [
|
||||
colorScheme.primaryContainer.withValues(alpha: 0.12),
|
||||
colorScheme.primary.withValues(alpha: 0.08),
|
||||
colorScheme.primaryContainer.withValues(alpha: 0.12),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
return const SizedBox.shrink(key: homeEmptyStateAmbientKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
|
||||
const homeBackgroundFieldKey = ValueKey('home_background_field');
|
||||
const homeTopGlowKey = ValueKey('home_top_glow');
|
||||
const homeBottomGlowKey = ValueKey('home_bottom_glow');
|
||||
|
||||
class HomeBackgroundField extends StatelessWidget {
|
||||
const HomeBackgroundField({super.key});
|
||||
@@ -22,72 +18,7 @@ class HomeBackgroundField extends StatelessWidget {
|
||||
colors: [colorScheme.surface, colorScheme.surfaceContainerLowest],
|
||||
),
|
||||
),
|
||||
child: const Stack(children: [_HomeTopGlow(), _HomeBottomGlow()]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HomeTopGlow extends StatelessWidget {
|
||||
const _HomeTopGlow();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Align(
|
||||
alignment: const Alignment(-0.25, -0.9),
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
key: homeTopGlowKey,
|
||||
width: AppSpacing.xxl * 10,
|
||||
height: AppSpacing.xxl * 7,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(AppSpacing.xxl * 3),
|
||||
color: colorScheme.primaryContainer.withValues(alpha: 0.28),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.primary.withValues(alpha: 0.2),
|
||||
blurRadius: AppSpacing.xxl * 3,
|
||||
spreadRadius: AppSpacing.xl,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HomeBottomGlow extends StatelessWidget {
|
||||
const _HomeBottomGlow();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return IgnorePointer(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Transform.translate(
|
||||
offset: const Offset(0, AppSpacing.lg),
|
||||
child: Container(
|
||||
key: homeBottomGlowKey,
|
||||
width: AppSpacing.xxl * 12,
|
||||
height: AppSpacing.xxl * 3,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primaryContainer.withValues(alpha: 0.18),
|
||||
borderRadius: BorderRadius.circular(AppSpacing.xxl * 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.primary.withValues(alpha: 0.1),
|
||||
blurRadius: AppSpacing.xxl,
|
||||
spreadRadius: AppSpacing.sm,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: const SizedBox.expand(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user