Skip to content

Commit 584f762

Browse files
authored
Merge pull request #7617 from richardshiue/chore/improve-model-selection-ui
feat: regenerate message with different model
2 parents 9147f64 + 8528811 commit 584f762

19 files changed

Lines changed: 779 additions & 446 deletions

frontend/appflowy_flutter/lib/ai/ai.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ export 'service/ai_entities.dart';
22
export 'service/ai_prompt_input_bloc.dart';
33
export 'service/appflowy_ai_service.dart';
44
export 'service/error.dart';
5+
export 'service/ai_model_state_notifier.dart';
6+
export 'service/select_model_bloc.dart';
57
export 'widgets/loading_indicator.dart';
68
export 'widgets/prompt_input/action_buttons.dart';
79
export 'widgets/prompt_input/desktop_prompt_text_field.dart';
@@ -13,4 +15,5 @@ export 'widgets/prompt_input/mentioned_page_text_span.dart';
1315
export 'widgets/prompt_input/predefined_format_buttons.dart';
1416
export 'widgets/prompt_input/select_sources_bottom_sheet.dart';
1517
export 'widgets/prompt_input/select_sources_menu.dart';
18+
export 'widgets/prompt_input/select_model_menu.dart';
1619
export 'widgets/prompt_input/send_button.dart';

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';
44
import 'package:easy_localization/easy_localization.dart';
55
import 'package:equatable/equatable.dart';
66

7+
enum AiType {
8+
cloud,
9+
local;
10+
11+
bool get isCloud => this == cloud;
12+
bool get isLocal => this == local;
13+
}
14+
715
class PredefinedFormat extends Equatable {
816
const PredefinedFormat({
917
required this.imageFormat,

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

Lines changed: 0 additions & 138 deletions
This file was deleted.
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import 'package:appflowy/ai/ai.dart';
2+
import 'package:appflowy/generated/locale_keys.g.dart';
3+
import 'package:appflowy/plugins/ai_chat/application/ai_model_switch_listener.dart';
4+
import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart';
5+
import 'package:appflowy_backend/dispatch/dispatch.dart';
6+
import 'package:appflowy_backend/log.dart';
7+
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
8+
import 'package:appflowy_result/appflowy_result.dart';
9+
import 'package:easy_localization/easy_localization.dart';
10+
import 'package:protobuf/protobuf.dart';
11+
import 'package:universal_platform/universal_platform.dart';
12+
13+
typedef OnModelStateChangedCallback = void Function(AiType, bool, String);
14+
typedef OnAvailableModelsChangedCallback = void Function(
15+
List<AIModelPB>,
16+
AIModelPB?,
17+
);
18+
19+
class AIModelStateNotifier {
20+
AIModelStateNotifier({required this.objectId})
21+
: _localAIListener =
22+
UniversalPlatform.isDesktop ? LocalAIStateListener() : null,
23+
_aiModelSwitchListener = AIModelSwitchListener(objectId: objectId) {
24+
_startListening();
25+
_init();
26+
}
27+
28+
final String objectId;
29+
final LocalAIStateListener? _localAIListener;
30+
final AIModelSwitchListener _aiModelSwitchListener;
31+
LocalAIPB? _localAIState;
32+
AvailableModelsPB? _availableModels;
33+
34+
// callbacks
35+
final List<OnModelStateChangedCallback> _stateChangedCallbacks = [];
36+
final List<OnAvailableModelsChangedCallback>
37+
_availableModelsChangedCallbacks = [];
38+
39+
void _startListening() {
40+
if (UniversalPlatform.isDesktop) {
41+
_localAIListener?.start(
42+
stateCallback: (state) async {
43+
_localAIState = state;
44+
_notifyStateChanged();
45+
46+
if (state.state == RunningStatePB.Running ||
47+
state.state == RunningStatePB.Stopped) {
48+
await _loadAvailableModels();
49+
_notifyAvailableModelsChanged();
50+
}
51+
},
52+
);
53+
}
54+
55+
_aiModelSwitchListener.start(
56+
onUpdateSelectedModel: (model) async {
57+
final updatedModels = _availableModels?.deepCopy()
58+
?..selectedModel = model;
59+
_availableModels = updatedModels;
60+
_notifyAvailableModelsChanged();
61+
62+
if (model.isLocal && UniversalPlatform.isDesktop) {
63+
await _loadLocalAiState();
64+
}
65+
_notifyStateChanged();
66+
},
67+
);
68+
}
69+
70+
void _init() async {
71+
await Future.wait([_loadLocalAiState(), _loadAvailableModels()]);
72+
_notifyStateChanged();
73+
_notifyAvailableModelsChanged();
74+
}
75+
76+
void addListener({
77+
OnModelStateChangedCallback? onStateChanged,
78+
OnAvailableModelsChangedCallback? onAvailableModelsChanged,
79+
}) {
80+
if (onStateChanged != null) {
81+
_stateChangedCallbacks.add(onStateChanged);
82+
}
83+
if (onAvailableModelsChanged != null) {
84+
_availableModelsChangedCallbacks.add(onAvailableModelsChanged);
85+
}
86+
}
87+
88+
void removeListener({
89+
OnModelStateChangedCallback? onStateChanged,
90+
OnAvailableModelsChangedCallback? onAvailableModelsChanged,
91+
}) {
92+
if (onStateChanged != null) {
93+
_stateChangedCallbacks.remove(onStateChanged);
94+
}
95+
if (onAvailableModelsChanged != null) {
96+
_availableModelsChangedCallbacks.remove(onAvailableModelsChanged);
97+
}
98+
}
99+
100+
Future<void> dispose() async {
101+
_stateChangedCallbacks.clear();
102+
_availableModelsChangedCallbacks.clear();
103+
await _localAIListener?.stop();
104+
await _aiModelSwitchListener.stop();
105+
}
106+
107+
(AiType, String, bool) getState() {
108+
if (UniversalPlatform.isMobile) {
109+
return (AiType.cloud, LocaleKeys.chat_inputMessageHint.tr(), true);
110+
}
111+
112+
final availableModels = _availableModels;
113+
final localAiState = _localAIState;
114+
115+
if (availableModels == null) {
116+
Log.warn("No available models");
117+
return (AiType.cloud, LocaleKeys.chat_inputMessageHint.tr(), true);
118+
}
119+
if (localAiState == null) {
120+
Log.warn("Cannot get local AI state");
121+
return (AiType.cloud, LocaleKeys.chat_inputMessageHint.tr(), true);
122+
}
123+
124+
if (!availableModels.selectedModel.isLocal) {
125+
return (AiType.cloud, LocaleKeys.chat_inputMessageHint.tr(), true);
126+
}
127+
128+
final editable = localAiState.state == RunningStatePB.Running;
129+
final hintText = editable
130+
? LocaleKeys.chat_inputLocalAIMessageHint.tr()
131+
: LocaleKeys.settings_aiPage_keys_localAIInitializing.tr();
132+
133+
return (AiType.local, hintText, editable);
134+
}
135+
136+
(List<AIModelPB>, AIModelPB?) getAvailableModels() {
137+
final availableModels = _availableModels;
138+
if (availableModels == null) {
139+
return ([], null);
140+
}
141+
return (availableModels.models, availableModels.selectedModel);
142+
}
143+
144+
void _notifyAvailableModelsChanged() {
145+
final (models, selectedModel) = getAvailableModels();
146+
for (final callback in _availableModelsChangedCallbacks) {
147+
callback(models, selectedModel);
148+
}
149+
}
150+
151+
void _notifyStateChanged() {
152+
final (type, hintText, isEditable) = getState();
153+
for (final callback in _stateChangedCallbacks) {
154+
callback(type, isEditable, hintText);
155+
}
156+
}
157+
158+
Future<void> _loadAvailableModels() {
159+
final payload = AvailableModelsQueryPB(source: objectId);
160+
return AIEventGetAvailableModels(payload).send().fold(
161+
(models) => _availableModels = models,
162+
(err) => Log.error("Failed to get available models: $err"),
163+
);
164+
}
165+
166+
Future<void> _loadLocalAiState() {
167+
return AIEventGetLocalAIState().send().fold(
168+
(localAIState) => _localAIState = localAIState,
169+
(error) => Log.error("Failed to get local AI state: $error"),
170+
);
171+
}
172+
}

0 commit comments

Comments
 (0)