a687e3637f
Migrate StateNotifierProvider/StateNotifier → NotifierProvider/Notifier, StateProvider → NotifierProvider with simple notifier, FamilyAsyncNotifier removed in Riverpod 3 (replaced with constructor-arg pattern), and rename .valueOrNull → .value throughout all providers and screens. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
120 lines
3.9 KiB
Dart
120 lines
3.9 KiB
Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../data/models/briefing_conversation.dart';
|
|
import '../data/models/message.dart';
|
|
import 'api_client_provider.dart';
|
|
|
|
/// Drives the loading indicator in BriefingScreen's reply area.
|
|
final isBriefingStreamingProvider =
|
|
NotifierProvider<_BoolNotifier, bool>(_BoolNotifier.new);
|
|
|
|
class _BoolNotifier extends Notifier<bool> {
|
|
@override
|
|
bool build() => false;
|
|
}
|
|
|
|
final briefingProvider =
|
|
AsyncNotifierProvider<BriefingNotifier, BriefingConversation>(
|
|
BriefingNotifier.new);
|
|
|
|
class BriefingNotifier extends AsyncNotifier<BriefingConversation> {
|
|
@override
|
|
Future<BriefingConversation> build() async {
|
|
return ref.read(briefingApiProvider).getToday();
|
|
}
|
|
|
|
/// Trigger a briefing slot (e.g. "compilation") then reload.
|
|
Future<void> refresh(String slot) async {
|
|
await ref.read(briefingApiProvider).triggerSlot(slot);
|
|
ref.invalidateSelf();
|
|
await future;
|
|
}
|
|
|
|
/// Send a reply to today's briefing conversation.
|
|
///
|
|
/// Mirrors MessagesNotifier.sendMessage():
|
|
/// 1. Optimistic UI update
|
|
/// 2. POST message to chat endpoint
|
|
/// 3. SSE stream (best-effort)
|
|
/// 4. Poll until complete
|
|
Future<void> sendReply(String content) async {
|
|
final conv = state.value;
|
|
if (conv == null) return;
|
|
final convId = conv.id;
|
|
final chatApi = ref.read(chatApiProvider);
|
|
|
|
final previous = conv.messages;
|
|
final userMsg = Message(
|
|
conversationId: convId,
|
|
role: MessageRole.user,
|
|
content: content,
|
|
);
|
|
final placeholder = Message(
|
|
conversationId: convId,
|
|
role: MessageRole.assistant,
|
|
content: '',
|
|
status: 'generating',
|
|
);
|
|
state = AsyncData(conv.copyWith(messages: [...previous, userMsg, placeholder]));
|
|
ref.read(isBriefingStreamingProvider.notifier).state = true;
|
|
|
|
try {
|
|
await chatApi.sendMessage(convId, content);
|
|
} catch (e) {
|
|
state = AsyncData(conv.copyWith(messages: previous));
|
|
ref.read(isBriefingStreamingProvider.notifier).state = false;
|
|
rethrow;
|
|
}
|
|
|
|
// SSE stream (best-effort)
|
|
bool streamedContent = false;
|
|
try {
|
|
await for (final chunk in chatApi.streamGeneration(convId)) {
|
|
streamedContent = true;
|
|
final current = state.value;
|
|
if (current == null) break;
|
|
final msgs = current.messages;
|
|
if (msgs.isEmpty) continue;
|
|
final updated = msgs.last.copyWith(content: msgs.last.content + chunk);
|
|
state = AsyncData(current.copyWith(
|
|
messages: [...msgs.sublist(0, msgs.length - 1), updated]));
|
|
}
|
|
} catch (_) {
|
|
// Fall through to polling.
|
|
}
|
|
|
|
// Poll until complete (max 20 attempts, 2s apart)
|
|
try {
|
|
for (var attempt = 0; attempt < 20; attempt++) {
|
|
if (attempt > 0) await Future.delayed(const Duration(seconds: 2));
|
|
final fresh = await ref.read(briefingApiProvider).getMessages(convId);
|
|
final done = fresh.any(
|
|
(m) => m.role == MessageRole.assistant && m.status != 'generating',
|
|
);
|
|
final hasContent = fresh.any(
|
|
(m) => m.role == MessageRole.assistant && m.content.isNotEmpty,
|
|
);
|
|
final current = state.value;
|
|
if (current != null && (!streamedContent || done || hasContent)) {
|
|
state = AsyncData(current.copyWith(messages: fresh));
|
|
}
|
|
if (done) break;
|
|
}
|
|
} catch (_) {
|
|
// Clear the generating placeholder so UI doesn't spin forever.
|
|
final current = state.value;
|
|
if (current != null) {
|
|
final msgs = current.messages;
|
|
if (msgs.isNotEmpty && msgs.last.status == 'generating') {
|
|
state = AsyncData(current.copyWith(messages: [
|
|
...msgs.sublist(0, msgs.length - 1),
|
|
msgs.last.copyWith(status: 'complete'),
|
|
]));
|
|
}
|
|
}
|
|
} finally {
|
|
ref.read(isBriefingStreamingProvider.notifier).state = false;
|
|
}
|
|
}
|
|
}
|