sendMessage method
- String userMessage
Send a user message, call Claude, and update state.
Implementation
Future<void> sendMessage(String userMessage) async {
// Append user message.
final userMsg = ChatMessage(role: 'user', content: userMessage);
state = state.copyWith(
messages: [...state.messages, userMsg],
isLoading: true,
error: null,
);
try {
final supabase = ref.read(supabaseDatasourceProvider);
// Auto-create a session on first message.
if (_sessionId == null) {
final sessionType = _modeToSessionType(params.mode);
_sessionStartedAt = DateTime.now();
final session = await supabase.createSession({
'session_type': sessionType,
if (params.topicId != null) 'topic_id': params.topicId,
'started_at': _sessionStartedAt!.toIso8601String(),
'is_validation': params.isValidation,
if (params.isValidation && params.validationTarget != null)
'validation_target': params.validationTarget,
});
_sessionId = session.id;
}
// Persist the user message.
await supabase.saveMessage(_sessionId!, 'user', userMessage);
// Resolve AI datasource.
final ai = _resolveAi();
// When using the proxy, skip client-side prompt building and chunk
// retrieval — the proxy handles both server-side.
final String fullSystemPrompt;
if (ai is ProxyAiDatasource) {
fullSystemPrompt = ''; // Ignored by ProxyAiDatasource.
} else {
// Build system prompt based on mode (or validation variant).
final systemPrompt = params.isValidation
? _buildValidationSystemPrompt()
: _buildSystemPrompt();
// Load content chunks via adaptive retrieval.
String? chunkContext;
if (params.topicId != null) {
final retriever = ref.read(contentRetrieverProvider);
final chunks = await retriever.retrieveChunks(
mode: params.mode,
topicId: params.topicId,
userMessage: userMessage,
);
if (chunks.isNotEmpty) {
final buf = StringBuffer();
for (final c in chunks) {
if (buf.length + c.content.length >
RetrievalConfig.maxChunkChars) {
break;
}
if (buf.isNotEmpty) buf.write('\n\n');
buf.write(c.content);
}
chunkContext = buf.toString();
}
}
fullSystemPrompt = chunkContext != null
? '$systemPrompt\n\n<reference_material>\n$chunkContext\n</reference_material>'
: systemPrompt;
}
// Build message list for the API, with truncation for long conversations.
var apiMessages = state.messages
.map((m) => {'role': m.role, 'content': m.content})
.toList();
// Truncate conversation history if too long: keep first + most recent.
if (apiMessages.length > 2) {
int totalChars = apiMessages.fold(
0,
(sum, m) => sum + (m['content']?.length ?? 0),
);
while (totalChars > RetrievalConfig.maxConversationChars &&
apiMessages.length > 2) {
final removed = apiMessages.removeAt(1);
totalChars -= removed['content']?.length ?? 0;
}
}
// Call AI.
final response = await ai.sendMessage(
systemPrompt: fullSystemPrompt,
messages: apiMessages,
);
// Append assistant message.
final assistantMsg = ChatMessage(role: 'assistant', content: response);
state = state.copyWith(
messages: [...state.messages, assistantMsg],
isLoading: false,
);
// Persist assistant message.
await supabase.saveMessage(_sessionId!, 'assistant', response);
} catch (e) {
state = state.copyWith(
isLoading: false,
error: userFriendlyMessage(e, logName: 'ChatNotifier'),
);
}
}