Persist regenerated answer versions
This commit is contained in:
@@ -91,9 +91,19 @@ class ApiRepository {
|
||||
messages:
|
||||
(data['messages'] as List<dynamic>).map((item) {
|
||||
final message = item as Map<String, dynamic>;
|
||||
final content = message['content'] as String;
|
||||
final fromUser = message['role'] == 'user';
|
||||
final versions =
|
||||
(message['versions'] as List<dynamic>? ?? const [])
|
||||
.cast<String>();
|
||||
return ChatMessage(
|
||||
message['content'] as String,
|
||||
fromUser: message['role'] == 'user',
|
||||
content,
|
||||
fromUser: fromUser,
|
||||
answerVersions:
|
||||
fromUser
|
||||
? const []
|
||||
: (versions.isEmpty ? [content] : versions),
|
||||
selectedVersion: message['selected_version'] as int? ?? 0,
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
@@ -111,6 +121,13 @@ class ApiRepository {
|
||||
(message) => {
|
||||
'role': message.fromUser ? 'user' : 'assistant',
|
||||
'content': message.text,
|
||||
if (!message.fromUser) ...{
|
||||
'versions':
|
||||
message.answerVersions.isEmpty
|
||||
? [message.text]
|
||||
: message.answerVersions,
|
||||
'selected_version': message.selectedVersion,
|
||||
},
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
|
||||
+72
-4
@@ -120,14 +120,46 @@ class ChatCubit extends Cubit<ChatState> {
|
||||
!messages[messages.length - 2].fromUser) {
|
||||
return;
|
||||
}
|
||||
final previousAnswer = messages.last;
|
||||
final versions =
|
||||
previousAnswer.answerVersions.isEmpty
|
||||
? [previousAnswer.text]
|
||||
: previousAnswer.answerVersions;
|
||||
final history = messages.sublist(0, messages.length - 1);
|
||||
await _sendWithHistory(
|
||||
history.last.text,
|
||||
history,
|
||||
appendUserMessage: false,
|
||||
previousAnswers: versions,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> selectAnswerVersion(int messageIndex, int versionIndex) async {
|
||||
if (state.isSending ||
|
||||
messageIndex < 0 ||
|
||||
messageIndex >= state.messages.length) {
|
||||
return;
|
||||
}
|
||||
final messages = List<ChatMessage>.from(state.messages);
|
||||
final message = messages[messageIndex];
|
||||
final versions = message.answerVersions;
|
||||
if (message.fromUser || versionIndex < 0 || versionIndex >= versions.length) {
|
||||
return;
|
||||
}
|
||||
messages[messageIndex] = ChatMessage(
|
||||
versions[versionIndex],
|
||||
fromUser: false,
|
||||
answerVersions: versions,
|
||||
selectedVersion: versionIndex,
|
||||
);
|
||||
try {
|
||||
await _saveCurrent(messages);
|
||||
emit(state.copyWith(clearError: true));
|
||||
} catch (error) {
|
||||
emit(state.copyWith(error: 'تعذر حفظ نسخة الإجابة المختارة: $error'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> editLastQuestion(String text) async {
|
||||
if (state.isSending || text.trim().isEmpty) return;
|
||||
final messages = state.messages;
|
||||
@@ -146,6 +178,7 @@ class ChatCubit extends Cubit<ChatState> {
|
||||
String text,
|
||||
List<ChatMessage> history, {
|
||||
bool appendUserMessage = true,
|
||||
List<String> previousAnswers = const [],
|
||||
}) async {
|
||||
final prompt = text.trim();
|
||||
if (prompt.isEmpty || state.isSending || state.isLoadingHistory) return;
|
||||
@@ -173,7 +206,16 @@ class ChatCubit extends Cubit<ChatState> {
|
||||
if (answer.trim().isEmpty) {
|
||||
throw Exception('لم يصل نص في إجابة الوكيل.');
|
||||
}
|
||||
final updated = [...messages, ChatMessage(answer, fromUser: false)];
|
||||
final answerVersions = [...previousAnswers, answer];
|
||||
final updated = [
|
||||
...messages,
|
||||
ChatMessage(
|
||||
answer,
|
||||
fromUser: false,
|
||||
answerVersions: answerVersions,
|
||||
selectedVersion: answerVersions.length - 1,
|
||||
),
|
||||
];
|
||||
emit(state.copyWith(messages: updated, isSending: false));
|
||||
await _saveCurrent(updated);
|
||||
return;
|
||||
@@ -184,15 +226,33 @@ class ChatCubit extends Cubit<ChatState> {
|
||||
)) {
|
||||
if (isClosed) return;
|
||||
answer += delta;
|
||||
final answerVersions = [...previousAnswers, answer];
|
||||
emit(
|
||||
state.copyWith(
|
||||
messages: [...messages, ChatMessage(answer, fromUser: false)],
|
||||
messages: [
|
||||
...messages,
|
||||
ChatMessage(
|
||||
answer,
|
||||
fromUser: false,
|
||||
answerVersions: answerVersions,
|
||||
selectedVersion: answerVersions.length - 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
if (isClosed) return;
|
||||
if (answer.trim().isEmpty) throw Exception('لم يصل نص في الإجابة.');
|
||||
final updated = [...messages, ChatMessage(answer, fromUser: false)];
|
||||
final answerVersions = [...previousAnswers, answer];
|
||||
final updated = [
|
||||
...messages,
|
||||
ChatMessage(
|
||||
answer,
|
||||
fromUser: false,
|
||||
answerVersions: answerVersions,
|
||||
selectedVersion: answerVersions.length - 1,
|
||||
),
|
||||
];
|
||||
emit(state.copyWith(messages: updated, isSending: false));
|
||||
await _saveCurrent(updated);
|
||||
} catch (error) {
|
||||
@@ -202,7 +262,15 @@ class ChatCubit extends Cubit<ChatState> {
|
||||
? originalMessages
|
||||
: answer.isEmpty
|
||||
? messages
|
||||
: [...messages, ChatMessage(answer, fromUser: false)];
|
||||
: [
|
||||
...messages,
|
||||
ChatMessage(
|
||||
answer,
|
||||
fromUser: false,
|
||||
answerVersions: [...previousAnswers, answer],
|
||||
selectedVersion: previousAnswers.length,
|
||||
),
|
||||
];
|
||||
emit(
|
||||
state.copyWith(
|
||||
messages: partial,
|
||||
|
||||
+21
-3
@@ -1,13 +1,31 @@
|
||||
class ChatMessage {
|
||||
const ChatMessage(this.text, {required this.fromUser});
|
||||
const ChatMessage(
|
||||
this.text, {
|
||||
required this.fromUser,
|
||||
this.answerVersions = const [],
|
||||
this.selectedVersion = 0,
|
||||
});
|
||||
|
||||
final String text;
|
||||
final bool fromUser;
|
||||
final List<String> answerVersions;
|
||||
final int selectedVersion;
|
||||
|
||||
Map<String, dynamic> toJson() => {'text': text, 'fromUser': fromUser};
|
||||
Map<String, dynamic> toJson() => {
|
||||
'text': text,
|
||||
'fromUser': fromUser,
|
||||
'answerVersions': answerVersions,
|
||||
'selectedVersion': selectedVersion,
|
||||
};
|
||||
|
||||
factory ChatMessage.fromJson(Map<String, dynamic> json) =>
|
||||
ChatMessage(json['text'] as String, fromUser: json['fromUser'] as bool);
|
||||
ChatMessage(
|
||||
json['text'] as String,
|
||||
fromUser: json['fromUser'] as bool,
|
||||
answerVersions:
|
||||
(json['answerVersions'] as List<dynamic>? ?? const []).cast<String>(),
|
||||
selectedVersion: json['selectedVersion'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
class SavedConversation {
|
||||
|
||||
@@ -710,6 +710,52 @@ class _ChatPageState extends State<ChatPage> {
|
||||
)
|
||||
else
|
||||
_AssistantMarkdown(text: message.text),
|
||||
if (!message.fromUser && message.answerVersions.length > 1)
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: 'نسخة الإجابة السابقة',
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed:
|
||||
chat.isSending || message.selectedVersion == 0
|
||||
? null
|
||||
: () => context
|
||||
.read<ChatCubit>()
|
||||
.selectAnswerVersion(
|
||||
index,
|
||||
message.selectedVersion - 1,
|
||||
),
|
||||
icon: const Icon(Icons.chevron_right_rounded),
|
||||
),
|
||||
Text(
|
||||
'النسخة ${message.selectedVersion + 1} من ${message.answerVersions.length}',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF718078),
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'نسخة الإجابة التالية',
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed:
|
||||
chat.isSending ||
|
||||
message.selectedVersion >=
|
||||
message.answerVersions.length - 1
|
||||
? null
|
||||
: () => context
|
||||
.read<ChatCubit>()
|
||||
.selectAnswerVersion(
|
||||
index,
|
||||
message.selectedVersion + 1,
|
||||
),
|
||||
icon: const Icon(Icons.chevron_left_rounded),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!chat.isSending &&
|
||||
!showTyping &&
|
||||
index == chat.messages.length - 1 &&
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:flutter_app/core/network/api_repository.dart';
|
||||
import 'package:flutter_app/features/chat/presentation/cubit/chat_cubit.dart';
|
||||
import 'package:flutter_app/features/chat/presentation/cubit/chat_state.dart';
|
||||
|
||||
class _VersionTestApi extends ApiRepository {
|
||||
_VersionTestApi() : super(baseUrl: 'http://127.0.0.1:8000');
|
||||
|
||||
final responses = <List<String>>[
|
||||
['الجواب الأول'],
|
||||
['الجواب الثاني'],
|
||||
];
|
||||
SavedConversation? lastSaved;
|
||||
|
||||
@override
|
||||
Future<String> getModelName() async => 'gemma4:e2b';
|
||||
|
||||
@override
|
||||
Future<List<String>> getAvailableModels() async => ['gemma4:e2b'];
|
||||
|
||||
@override
|
||||
Future<List<SavedConversation>> listConversations() async => [];
|
||||
|
||||
@override
|
||||
Future<void> saveConversation(SavedConversation conversation) async {
|
||||
lastSaved = conversation;
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<String> completeStream(
|
||||
List<ChatMessage> messages, {
|
||||
String? model,
|
||||
}) => Stream<String>.fromIterable(responses.removeAt(0));
|
||||
}
|
||||
|
||||
void main() {
|
||||
test('regeneration keeps prior answers and selection is persisted', () async {
|
||||
final api = _VersionTestApi();
|
||||
final cubit = ChatCubit(api);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
await cubit.send('ما معنى API؟');
|
||||
expect(cubit.state.messages.last.answerVersions, ['الجواب الأول']);
|
||||
|
||||
await cubit.regenerateLastAnswer();
|
||||
expect(cubit.state.messages.last.text, 'الجواب الثاني');
|
||||
expect(cubit.state.messages.last.answerVersions, ['الجواب الأول', 'الجواب الثاني']);
|
||||
expect(cubit.state.messages.last.selectedVersion, 1);
|
||||
|
||||
await cubit.selectAnswerVersion(1, 0);
|
||||
expect(cubit.state.messages.last.text, 'الجواب الأول');
|
||||
expect(cubit.state.messages.last.selectedVersion, 0);
|
||||
expect(api.lastSaved?.messages.last.answerVersions, ['الجواب الأول', 'الجواب الثاني']);
|
||||
expect(api.lastSaved?.messages.last.selectedVersion, 0);
|
||||
|
||||
await cubit.close();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user