Skip to content

Commit 4092970

Browse files
committed
feat: pre-defined response formats
1 parent c05f19e commit 4092970

File tree

13 files changed

+1026
-45
lines changed

13 files changed

+1026
-45
lines changed

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
231231
),
232232
);
233233
},
234-
regenerateAnswer: (id) {
234+
regenerateAnswer: (id, format) {
235235
_clearRelatedQuestions();
236236
_regenerateAnswer(id);
237237
lastSentMessage = null;
@@ -592,7 +592,10 @@ class ChatEvent with _$ChatEvent {
592592
const factory ChatEvent.failedSending() = _FailSendMessage;
593593

594594
// regenerate
595-
const factory ChatEvent.regenerateAnswer(String id) = _RegenerateAnswer;
595+
const factory ChatEvent.regenerateAnswer(
596+
String id,
597+
(PredefinedFormat, PredefinedTextFormat?)? format,
598+
) = _RegenerateAnswer;
596599

597600
// streaming answer
598601
const factory ChatEvent.stopStream() = _StopStream;

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import 'dart:io';
22

3+
import 'package:appflowy/generated/flowy_svgs.g.dart';
4+
import 'package:appflowy/generated/locale_keys.g.dart';
35
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pbenum.dart';
46
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
57
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
8+
import 'package:easy_localization/easy_localization.dart';
69
import 'package:equatable/equatable.dart';
710
import 'package:freezed_annotation/freezed_annotation.dart';
811
import 'package:path/path.dart' as path;
@@ -137,3 +140,55 @@ enum LoadChatMessageStatus {
137140
loadingRemote,
138141
ready,
139142
}
143+
144+
enum PredefinedFormat {
145+
text,
146+
image,
147+
textAndImage;
148+
149+
bool get hasText => this == text || this == textAndImage;
150+
151+
FlowySvgData get icon {
152+
return switch (this) {
153+
PredefinedFormat.text => FlowySvgs.ai_text_s,
154+
PredefinedFormat.image => FlowySvgs.ai_image_s,
155+
PredefinedFormat.textAndImage => FlowySvgs.ai_text_image_s,
156+
};
157+
}
158+
159+
String get i18n {
160+
return switch (this) {
161+
PredefinedFormat.text => LocaleKeys.chat_changeFormat_textOnly.tr(),
162+
PredefinedFormat.image => LocaleKeys.chat_changeFormat_imageOnly.tr(),
163+
PredefinedFormat.textAndImage =>
164+
LocaleKeys.chat_changeFormat_textAndImage.tr(),
165+
};
166+
}
167+
}
168+
169+
enum PredefinedTextFormat {
170+
auto,
171+
bulletList,
172+
numberedList,
173+
table;
174+
175+
FlowySvgData get icon {
176+
return switch (this) {
177+
PredefinedTextFormat.auto => FlowySvgs.ai_paragraph_s,
178+
PredefinedTextFormat.bulletList => FlowySvgs.ai_list_s,
179+
PredefinedTextFormat.numberedList => FlowySvgs.ai_number_list_s,
180+
PredefinedTextFormat.table => FlowySvgs.ai_table_s,
181+
};
182+
}
183+
184+
String get i18n {
185+
return switch (this) {
186+
PredefinedTextFormat.auto => LocaleKeys.chat_changeFormat_text.tr(),
187+
PredefinedTextFormat.bulletList =>
188+
LocaleKeys.chat_changeFormat_bullet.tr(),
189+
PredefinedTextFormat.numberedList =>
190+
LocaleKeys.chat_changeFormat_number.tr(),
191+
PredefinedTextFormat.table => LocaleKeys.chat_changeFormat_table.tr(),
192+
};
193+
}
194+
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,10 @@ class _ChatContentPage extends StatelessWidget {
214214
_onSelectMetadata(context, metadata),
215215
onRegenerate: () => context
216216
.read<ChatBloc>()
217-
.add(ChatEvent.regenerateAnswer(message.id)),
217+
.add(ChatEvent.regenerateAnswer(message.id, null)),
218+
onChangeFormat: (p0, p1) => context
219+
.read<ChatBloc>()
220+
.add(ChatEvent.regenerateAnswer(message.id, (p0, p1))),
218221
);
219222
},
220223
);

frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/desktop_ai_prompt_input.dart

Lines changed: 102 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ import 'package:flutter/material.dart';
1111
import 'package:flutter/services.dart';
1212
import 'package:flutter_bloc/flutter_bloc.dart';
1313

14+
import '../../application/chat_entity.dart';
1415
import '../layout_define.dart';
1516
import 'ai_prompt_buttons.dart';
1617
import 'chat_input_file.dart';
1718
import 'chat_input_span.dart';
1819
import 'chat_mention_page_menu.dart';
20+
import 'predefined_format_buttons.dart';
1921
import 'select_sources_menu.dart';
2022

2123
class DesktopAIPromptInput extends StatefulWidget {
@@ -46,6 +48,9 @@ class _DesktopAIPromptInputState extends State<DesktopAIPromptInput> {
4648
final focusNode = FocusNode();
4749
final textController = TextEditingController();
4850

51+
bool showPredefinedFormatSection = false;
52+
PredefinedFormat predefinedFormat = PredefinedFormat.text;
53+
PredefinedTextFormat? predefinedTextFormat = PredefinedTextFormat.auto;
4954
late SendButtonState sendButtonState;
5055

5156
@override
@@ -115,6 +120,7 @@ class _DesktopAIPromptInputState extends State<DesktopAIPromptInput> {
115120
? Theme.of(context).colorScheme.primary
116121
: Theme.of(context).colorScheme.outline,
117122
width: focusNode.hasFocus ? 1.5 : 1.0,
123+
strokeAlign: BorderSide.strokeAlignOutside,
118124
),
119125
borderRadius: DesktopAIPromptSizes.promptFrameRadius,
120126
),
@@ -136,24 +142,60 @@ class _DesktopAIPromptInputState extends State<DesktopAIPromptInput> {
136142
),
137143
),
138144
),
145+
const VSpace(4.0),
139146
Stack(
140147
children: [
141-
ConstrainedBox(
142-
constraints: BoxConstraints(
143-
minHeight: DesktopAIPromptSizes.textFieldMinHeight +
144-
DesktopAIPromptSizes.actionBarHeight +
145-
DesktopAIPromptSizes.actionBarPadding.vertical,
146-
maxHeight: 300,
147-
),
148+
Container(
149+
constraints: getTextFieldConstraints(),
148150
child: inputTextField(),
149151
),
152+
if (showPredefinedFormatSection)
153+
Positioned.fill(
154+
bottom: null,
155+
child: TextFieldTapRegion(
156+
child: Padding(
157+
padding:
158+
const EdgeInsetsDirectional.only(start: 8.0),
159+
child: ChangeFormatBar(
160+
predefinedFormat: predefinedFormat,
161+
predefinedTextFormat: predefinedTextFormat,
162+
spacing: DesktopAIPromptSizes
163+
.predefinedFormatBarButtonSpacing,
164+
iconSize: DesktopAIPromptSizes
165+
.predefinedFormatIconHeight,
166+
buttonSize: DesktopAIPromptSizes
167+
.predefinedFormatButtonHeight,
168+
onSelectPredefinedFormat: (p0, p1) {
169+
setState(() {
170+
predefinedFormat = p0;
171+
predefinedTextFormat = p1;
172+
});
173+
},
174+
),
175+
),
176+
),
177+
),
150178
Positioned.fill(
151179
top: null,
152180
child: TextFieldTapRegion(
153181
child: _PromptBottomActions(
154182
textController: textController,
155183
overlayController: overlayController,
156184
focusNode: focusNode,
185+
showPredefinedFormats: showPredefinedFormatSection,
186+
predefinedFormat: predefinedFormat,
187+
predefinedTextFormat: predefinedTextFormat,
188+
onTogglePredefinedFormatSection: () {
189+
setState(() {
190+
showPredefinedFormatSection =
191+
!showPredefinedFormatSection;
192+
if (!showPredefinedFormatSection) {
193+
predefinedFormat = PredefinedFormat.text;
194+
predefinedTextFormat =
195+
PredefinedTextFormat.auto;
196+
}
197+
});
198+
},
157199
sendButtonState: sendButtonState,
158200
onSendPressed: handleSendPressed,
159201
onStopStreaming: widget.onStopStreaming,
@@ -172,6 +214,18 @@ class _DesktopAIPromptInputState extends State<DesktopAIPromptInput> {
172214
);
173215
}
174216

217+
BoxConstraints getTextFieldConstraints() {
218+
double minHeight = DesktopAIPromptSizes.textFieldMinHeight +
219+
DesktopAIPromptSizes.actionBarHeight +
220+
DesktopAIPromptSizes.actionBarPadding.vertical;
221+
double maxHeight = 300;
222+
if (showPredefinedFormatSection) {
223+
minHeight += DesktopAIPromptSizes.predefinedFormatButtonHeight;
224+
maxHeight += DesktopAIPromptSizes.predefinedFormatButtonHeight;
225+
}
226+
return BoxConstraints(minHeight: minHeight, maxHeight: maxHeight);
227+
}
228+
175229
void cancelMentionPage() {
176230
if (overlayController.isShowing) {
177231
inputControlCubit.reset();
@@ -202,7 +256,13 @@ class _DesktopAIPromptInputState extends State<DesktopAIPromptInput> {
202256
}
203257

204258
// get the attached files and mentioned pages
205-
final metadata = context.read<AIPromptInputBloc>().consumeMetadata();
259+
final metadata = {
260+
...context.read<AIPromptInputBloc>().consumeMetadata(),
261+
if (showPredefinedFormatSection) ...{
262+
"format": predefinedFormat,
263+
"textFormat": predefinedTextFormat,
264+
},
265+
};
206266

207267
widget.onSubmitted(trimmedText, metadata);
208268
}
@@ -301,6 +361,7 @@ class _DesktopAIPromptInputState extends State<DesktopAIPromptInput> {
301361
cubit: inputControlCubit,
302362
textController: textController,
303363
textFieldFocusNode: focusNode,
364+
showPredefinedFormatSection: showPredefinedFormatSection,
304365
hintText: switch (state.aiType) {
305366
AIType.appflowyAI => LocaleKeys.chat_inputMessageHint.tr(),
306367
AIType.localAI => LocaleKeys.chat_inputLocalAIMessageHint.tr()
@@ -366,15 +427,17 @@ class _PromptTextField extends StatefulWidget {
366427
required this.cubit,
367428
required this.textController,
368429
required this.textFieldFocusNode,
369-
// required this.onStartMentioningPage,
430+
this.showPredefinedFormatSection = false,
370431
this.hintText = "",
432+
// this.onStartMentioningPage,
371433
});
372434

373435
final ChatInputControlCubit cubit;
374436
final TextEditingController textController;
375437
final FocusNode textFieldFocusNode;
376-
// final void Function() onStartMentioningPage;
438+
final bool showPredefinedFormatSection;
377439
final String hintText;
440+
// final void Function()? onStartMentioningPage;
378441

379442
@override
380443
State<_PromptTextField> createState() => _PromptTextFieldState();
@@ -408,11 +471,7 @@ class _PromptTextFieldState extends State<_PromptTextField> {
408471
border: InputBorder.none,
409472
enabledBorder: InputBorder.none,
410473
focusedBorder: InputBorder.none,
411-
contentPadding: DesktopAIPromptSizes.textFieldContentPadding.add(
412-
const EdgeInsets.only(
413-
bottom: DesktopAIPromptSizes.actionBarHeight,
414-
),
415-
),
474+
contentPadding: calculateContentPadding(),
416475
hintText: widget.hintText,
417476
hintStyle: Theme.of(context)
418477
.textTheme
@@ -450,6 +509,16 @@ class _PromptTextFieldState extends State<_PromptTextField> {
450509
);
451510
}
452511

512+
EdgeInsetsGeometry calculateContentPadding() {
513+
final top = widget.showPredefinedFormatSection
514+
? DesktopAIPromptSizes.predefinedFormatButtonHeight
515+
: 0.0;
516+
const bottom = DesktopAIPromptSizes.actionBarHeight;
517+
518+
return DesktopAIPromptSizes.textFieldContentPadding
519+
.add(EdgeInsets.only(top: top, bottom: bottom));
520+
}
521+
453522
Map<ShortcutActivator, Intent> buildShortcuts() {
454523
if (isComposing) {
455524
return const {};
@@ -486,6 +555,10 @@ class _PromptBottomActions extends StatelessWidget {
486555
required this.overlayController,
487556
required this.focusNode,
488557
required this.sendButtonState,
558+
required this.predefinedFormat,
559+
required this.predefinedTextFormat,
560+
required this.onTogglePredefinedFormatSection,
561+
required this.showPredefinedFormats,
489562
required this.onSendPressed,
490563
required this.onStopStreaming,
491564
required this.onUpdateSelectedSources,
@@ -494,6 +567,10 @@ class _PromptBottomActions extends StatelessWidget {
494567
final TextEditingController textController;
495568
final OverlayPortalController overlayController;
496569
final FocusNode focusNode;
570+
final bool showPredefinedFormats;
571+
final PredefinedFormat predefinedFormat;
572+
final PredefinedTextFormat? predefinedTextFormat;
573+
final void Function() onTogglePredefinedFormatSection;
497574
final SendButtonState sendButtonState;
498575
final void Function() onSendPressed;
499576
final void Function() onStopStreaming;
@@ -514,7 +591,7 @@ class _PromptBottomActions extends StatelessWidget {
514591
}
515592
return Row(
516593
children: [
517-
// predefinedFormatButton(),
594+
_predefinedFormatButton(),
518595
const Spacer(),
519596
if (state.aiType == AIType.appflowyAI) ...[
520597
_selectSourcesButton(context),
@@ -540,6 +617,15 @@ class _PromptBottomActions extends StatelessWidget {
540617
);
541618
}
542619

620+
Widget _predefinedFormatButton() {
621+
return PromptInputDesktopToggleFormatButton(
622+
showFormatBar: showPredefinedFormats,
623+
predefinedFormat: predefinedFormat,
624+
predefinedTextFormat: predefinedTextFormat,
625+
onTap: onTogglePredefinedFormatSection,
626+
);
627+
}
628+
543629
Widget _selectSourcesButton(BuildContext context) {
544630
return PromptInputDesktopSelectSourcesButton(
545631
onUpdateSelectedSources: onUpdateSelectedSources,

0 commit comments

Comments
 (0)