Skip to content

Commit c986e41

Browse files
appflowyLucasXu0
andauthored
Vault workspace suggested question (#7903)
* chore: return suggested question when ai say i don't know * chore: new message type * chore: implement uI * chore: implement UI * fix: assertion in local ai chat * chore: implement UI * chore: update prompt * chore: update logs * chore: add tests * chore: message id * fix: local ai page animation issue * chore: remove debug log * fix: remove repeated setState * chore: add test * chore: test * fix: compile --------- Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
1 parent c79ac32 commit c986e41

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+1193
-547
lines changed

frontend/appflowy_flutter/lib/ai/service/ai_entities.dart

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ class AIStreamEventPrefix {
1515
static const start = 'start:';
1616
static const finish = 'finish:';
1717
static const comment = 'comment:';
18-
static const aiResponseLimit = 'AI_RESPONSE_LIMIT';
19-
static const aiImageResponseLimit = 'AI_IMAGE_RESPONSE_LIMIT';
20-
static const aiMaxRequired = 'AI_MAX_REQUIRED:';
21-
static const localAINotReady = 'LOCAL_AI_NOT_READY';
22-
static const localAIDisabled = 'LOCAL_AI_DISABLED';
18+
static const aiResponseLimit = 'ai_response_limit:';
19+
static const aiImageResponseLimit = 'ai_image_response_limit:';
20+
static const aiMaxRequired = 'ai_max_required:';
21+
static const localAINotReady = 'local_ai_not_ready:';
22+
static const localAIDisabled = 'local_ai_disabled:';
23+
static const aiFollowUp = 'ai_follow_up:';
2324
}
2425

2526
enum AiType {

frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,14 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
117117
),
118118
);
119119
});
120+
121+
on<_OnAIFollowUp>((event, emit) {
122+
emit(
123+
state.copyWith(
124+
messageState: MessageState.aiFollowUp(event.followUpData),
125+
),
126+
);
127+
});
120128
}
121129

122130
void _initializeStreamListener() {
@@ -137,6 +145,9 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
137145
},
138146
onLocalAIInitializing: () =>
139147
_safeAdd(const ChatAIMessageEvent.onLocalAIInitializing()),
148+
onAIFollowUp: (data) {
149+
_safeAdd(ChatAIMessageEvent.onAIFollowUp(data));
150+
},
140151
);
141152
}
142153
}
@@ -174,6 +185,9 @@ class ChatAIMessageEvent with _$ChatAIMessageEvent {
174185
const factory ChatAIMessageEvent.receiveMetadata(
175186
MetadataCollection metadata,
176187
) = _ReceiveMetadata;
188+
const factory ChatAIMessageEvent.onAIFollowUp(
189+
AIFollowUpData followUpData,
190+
) = _OnAIFollowUp;
177191
}
178192

179193
@freezed
@@ -209,4 +223,6 @@ class MessageState with _$MessageState {
209223
const factory MessageState.onInitializingLocalAI() = _LocalAIInitializing;
210224
const factory MessageState.ready() = _Ready;
211225
const factory MessageState.loading() = _Loading;
226+
const factory MessageState.aiFollowUp(AIFollowUpData followUpData) =
227+
_AIFollowUp;
212228
}

frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ import 'chat_message_stream.dart';
2424

2525
part 'chat_bloc.freezed.dart';
2626

27+
/// Returns current Unix timestamp (seconds since epoch)
28+
int timestamp() {
29+
return DateTime.now().millisecondsSinceEpoch ~/ 1000;
30+
}
31+
2732
class ChatBloc extends Bloc<ChatEvent, ChatState> {
2833
ChatBloc({
2934
required this.chatId,
@@ -277,6 +282,10 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
277282
deleteMessage: (mesesage) async {
278283
await chatController.remove(mesesage);
279284
},
285+
onAIFollowUp: (followUpData) {
286+
shouldFetchRelatedQuestions =
287+
followUpData.shouldGenerateRelatedQuestion;
288+
},
280289
);
281290
},
282291
);
@@ -560,8 +569,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
560569
Map<String, dynamic>? sentMetadata,
561570
) {
562571
final now = DateTime.now();
563-
564-
questionStreamMessageId = (now.millisecondsSinceEpoch ~/ 1000).toString();
572+
questionStreamMessageId = timestamp().toString();
565573

566574
return TextMessage(
567575
author: User(id: userId),
@@ -674,6 +682,9 @@ class ChatEvent with _$ChatEvent {
674682
) = _DidReceiveRelatedQueston;
675683

676684
const factory ChatEvent.deleteMessage(Message message) = _DeleteMessage;
685+
686+
const factory ChatEvent.onAIFollowUp(AIFollowUpData followUpData) =
687+
_OnAIFollowUp;
677688
}
678689

679690
@freezed

frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_stream.dart

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import 'dart:async';
2+
import 'dart:convert';
23
import 'dart:ffi';
34
import 'dart:isolate';
45

56
import 'package:appflowy/ai/service/ai_entities.dart';
67
import 'package:appflowy/plugins/ai_chat/application/chat_message_service.dart';
8+
import 'package:appflowy_backend/log.dart';
9+
import 'package:json_annotation/json_annotation.dart';
10+
11+
part 'chat_message_stream.g.dart';
712

813
/// A stream that receives answer events from an isolate or external process.
914
/// It caches events that might occur before a listener is attached.
@@ -37,7 +42,7 @@ class AnswerStream {
3742
void Function()? _onAIImageResponseLimit;
3843
void Function(String message)? _onAIMaxRequired;
3944
void Function(MetadataCollection metadata)? _onMetadata;
40-
45+
void Function(AIFollowUpData)? _onAIFollowUp;
4146
// Caches for events that occur before listen() is called.
4247
final List<String> _pendingAIMaxRequiredEvents = [];
4348
bool _pendingLocalAINotReady = false;
@@ -88,6 +93,15 @@ class AnswerStream {
8893
} else {
8994
_pendingLocalAINotReady = true;
9095
}
96+
} else if (event.startsWith(AIStreamEventPrefix.aiFollowUp)) {
97+
final s = event.substring(AIStreamEventPrefix.aiFollowUp.length);
98+
try {
99+
final dynamic jsonData = jsonDecode(s);
100+
final data = AIFollowUpData.fromJson(jsonData);
101+
_onAIFollowUp?.call(data);
102+
} catch (e) {
103+
Log.error('Error deserializing AIFollowUp data: $e\nRaw JSON: $s');
104+
}
91105
}
92106
}
93107

@@ -114,6 +128,7 @@ class AnswerStream {
114128
void Function(String message)? onAIMaxRequired,
115129
void Function(MetadataCollection metadata)? onMetadata,
116130
void Function()? onLocalAIInitializing,
131+
void Function(AIFollowUpData)? onAIFollowUp,
117132
}) {
118133
_onData = onData;
119134
_onStart = onStart;
@@ -124,7 +139,7 @@ class AnswerStream {
124139
_onAIMaxRequired = onAIMaxRequired;
125140
_onMetadata = onMetadata;
126141
_onLocalAIInitializing = onLocalAIInitializing;
127-
142+
_onAIFollowUp = onAIFollowUp;
128143
// Flush pending AI_MAX_REQUIRED events.
129144
if (_onAIMaxRequired != null && _pendingAIMaxRequiredEvents.isNotEmpty) {
130145
for (final msg in _pendingAIMaxRequiredEvents) {
@@ -205,7 +220,6 @@ class QuestionStream {
205220
void Function()? _onIndexStart;
206221
void Function()? _onIndexEnd;
207222
void Function()? _onDone;
208-
209223
int get nativePort => _port.sendPort.nativePort;
210224
bool get hasStarted => _hasStarted;
211225
String? get error => _error;
@@ -241,3 +255,18 @@ class QuestionStream {
241255
_onDone = onDone;
242256
}
243257
}
258+
259+
@JsonSerializable()
260+
class AIFollowUpData {
261+
AIFollowUpData({
262+
required this.shouldGenerateRelatedQuestion,
263+
});
264+
265+
factory AIFollowUpData.fromJson(Map<String, dynamic> json) =>
266+
_$AIFollowUpDataFromJson(json);
267+
268+
@JsonKey(name: 'should_generate_related_question')
269+
final bool shouldGenerateRelatedQuestion;
270+
271+
Map<String, dynamic> toJson() => _$AIFollowUpDataToJson(this);
272+
}

frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/animated_chat_list.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ class ChatAnimatedListState extends State<ChatAnimatedList>
152152
itemPositionsListener.itemPositions.addListener(() {
153153
_handleLoadPreviousMessages();
154154
});
155+
156+
// A trick to avoid the first message being scrolled to the top
155157
}
156158

157159
@override
@@ -167,8 +169,8 @@ class ChatAnimatedListState extends State<ChatAnimatedList>
167169
Widget build(BuildContext context) {
168170
final builders = context.watch<Builders>();
169171
final height = MediaQuery.of(context).size.height;
170-
171172
// A trick to avoid the first message being scrolled to the top
173+
172174
initialScrollIndex = messages.length;
173175
initialAlignment = 1.0;
174176
if (messages.length <= 2) {

frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_page/text_message_widget.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ class TextMessageWidget extends StatelessWidget {
7979
selector: (state) => state.isSelectingMessages,
8080
builder: (context, isSelectingMessages) {
8181
return BlocBuilder<ChatBloc, ChatState>(
82+
buildWhen: (previous, current) =>
83+
previous.promptResponseState != current.promptResponseState,
8284
builder: (context, state) {
8385
final chatController = context.read<ChatBloc>().chatController;
8486
final messages = chatController.messages

0 commit comments

Comments
 (0)