3df85fdb04
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.
249 lines
7.3 KiB
Dart
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;
|
|
}
|