Files
ai-contract-platform/mobile/lib/features/chat/application/review_chat_controller.dart
T
yuqei 3df85fdb04 feat: land phase15 legal docs and commercial ops
Includes legal document generation workflows, prompt governance runtime wiring, legal change impact scans, operation KPI dashboards, invoice files, sales follow-up events, mobile review polish, deployment automation, and updated project docs.
2026-05-21 14:42:44 +08:00

249 lines
7.3 KiB
Dart

import 'package:flutter/foundation.dart';
import '../../../shared/network/api_client.dart';
import '../../../shared/storage/session_store.dart';
import '../domain/chat_models.dart';
import '../domain/review_chat_context.dart';
class ReviewChatController {
ReviewChatController({ApiClient? apiClient})
: _apiClient = apiClient ?? const ApiClient();
final ApiClient _apiClient;
final ValueNotifier<bool> isSending = ValueNotifier<bool>(false);
final ValueNotifier<bool> isLoadingConversations = ValueNotifier<bool>(false);
final ValueNotifier<String?> errorMessage = ValueNotifier<String?>(null);
final ValueNotifier<String?> conversationId = ValueNotifier<String?>(null);
final ValueNotifier<List<ChatConversationListItem>> conversations =
ValueNotifier<List<ChatConversationListItem>>(
<ChatConversationListItem>[]);
final ValueNotifier<List<ChatMessageItem>> messages =
ValueNotifier<List<ChatMessageItem>>(<ChatMessageItem>[]);
Future<void> loadConversations() async {
final session = SessionStore.current;
if (session == null || !session.isAuthenticated) {
conversations.value = <ChatConversationListItem>[];
return;
}
isLoadingConversations.value = true;
try {
conversations.value =
await _apiClient.listConversations(session.accessToken);
} on ApiException catch (error) {
if (error.isUnauthorized) {
await SessionStore.clear();
errorMessage.value = '登录已过期,请重新登录';
} else {
errorMessage.value = error.message;
}
} catch (_) {
errorMessage.value = '获取会话列表失败,请稍后重试';
} finally {
isLoadingConversations.value = false;
}
}
Future<void> openConversation(String targetConversationId) async {
final session = SessionStore.current;
if (session == null || !session.isAuthenticated) {
errorMessage.value = '请先登录后查看会话';
return;
}
isLoadingConversations.value = true;
errorMessage.value = null;
try {
final detail = await _apiClient.getConversationDetail(
session.accessToken,
targetConversationId,
);
conversationId.value = detail.id;
messages.value = detail.messages;
} on ApiException catch (error) {
if (error.isUnauthorized) {
await SessionStore.clear();
errorMessage.value = '登录已过期,请重新登录';
} else {
errorMessage.value = error.message;
}
} catch (_) {
errorMessage.value = '获取会话详情失败,请稍后重试';
} finally {
isLoadingConversations.value = false;
}
}
Future<void> sendMessage({
required String message,
ReviewChatContext? reviewContext,
}) async {
final normalizedMessage = message.trim();
if (normalizedMessage.isEmpty) {
errorMessage.value = '请输入问题后再发送';
return;
}
final session = SessionStore.current;
if (session == null || !session.isAuthenticated) {
errorMessage.value = '请先登录后再使用审查助手';
return;
}
if (isSending.value) {
return;
}
isSending.value = true;
errorMessage.value = null;
final userMessage = ChatMessageItem(
id: 'user-${DateTime.now().microsecondsSinceEpoch}',
role: 'user',
content: normalizedMessage,
);
final assistantMessage = ChatMessageItem(
id: 'assistant-${DateTime.now().microsecondsSinceEpoch}',
role: 'assistant',
content: '',
isStreaming: true,
);
messages.value = <ChatMessageItem>[
...messages.value,
userMessage,
assistantMessage
];
final assistantIndex = messages.value.length - 1;
try {
await _apiClient.streamChat(
accessToken: session.accessToken,
message: normalizedMessage,
conversationId: conversationId.value,
reviewContext: reviewContext?.toJson(),
onEvent: (event) {
switch (event.type) {
case 'text':
_appendAssistantText(assistantIndex, event.content ?? '');
break;
case 'sources':
_attachAssistantSources(
assistantIndex, event.sources ?? const <ChatSourceItem>[]);
break;
case 'end':
conversationId.value = event.conversationId;
_finishAssistantMessage(assistantIndex);
break;
case 'error':
throw ReviewChatException(event.message ?? '问答失败,请稍后重试');
}
},
);
_finishAssistantMessage(assistantIndex);
await loadConversations();
} on ApiException catch (error) {
if (error.isUnauthorized) {
await SessionStore.clear();
errorMessage.value = '登录已过期,请重新登录';
} else {
errorMessage.value = error.message;
}
_dropEmptyAssistantMessage(assistantIndex);
} on ReviewChatException catch (error) {
errorMessage.value = error.message;
_dropEmptyAssistantMessage(assistantIndex);
} catch (_) {
errorMessage.value = '问答失败,请检查网络后重试';
_dropEmptyAssistantMessage(assistantIndex);
} finally {
isSending.value = false;
}
}
void startNewConversation() {
conversationId.value = null;
errorMessage.value = null;
messages.value = <ChatMessageItem>[];
}
void _appendAssistantText(int index, String chunk) {
if (chunk.trim().isEmpty) {
return;
}
final current = messages.value;
if (index >= current.length) {
return;
}
final assistant = current[index];
final nextContent =
assistant.content.isEmpty ? chunk : '${assistant.content}\n\n$chunk';
final updated = assistant.copyWith(content: nextContent);
messages.value = <ChatMessageItem>[
...current.take(index),
updated,
...current.skip(index + 1),
];
}
void _attachAssistantSources(int index, List<ChatSourceItem> sources) {
final current = messages.value;
if (index >= current.length) {
return;
}
final assistant = current[index].copyWith(sources: sources);
messages.value = <ChatMessageItem>[
...current.take(index),
assistant,
...current.skip(index + 1),
];
}
void _finishAssistantMessage(int index) {
final current = messages.value;
if (index >= current.length) {
return;
}
final assistant = current[index].copyWith(isStreaming: false);
messages.value = <ChatMessageItem>[
...current.take(index),
assistant,
...current.skip(index + 1),
];
}
void _dropEmptyAssistantMessage(int index) {
final current = messages.value;
if (index >= current.length) {
return;
}
final assistant = current[index];
if (assistant.content.trim().isNotEmpty) {
_finishAssistantMessage(index);
return;
}
messages.value = <ChatMessageItem>[
...current.take(index),
...current.skip(index + 1),
];
}
void dispose() {
isSending.dispose();
isLoadingConversations.dispose();
errorMessage.dispose();
conversationId.dispose();
conversations.dispose();
messages.dispose();
}
}
class ReviewChatException implements Exception {
const ReviewChatException(this.message);
final String message;
@override
String toString() => message;
}