Skip to content

Commit

Permalink
Merge pull request #7 from 1runeberg/stage-v0.6.0
Browse files Browse the repository at this point in the history
Release v0.6.0
  • Loading branch information
1runeberg authored Aug 31, 2024
2 parents 2a9d6d6 + 5d6acf5 commit 4b63f31
Show file tree
Hide file tree
Showing 2 changed files with 198 additions and 111 deletions.
79 changes: 52 additions & 27 deletions confichat/lib/ui_canvass.dart
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,11 @@ class CanvassState extends State<Canvass> {
itemBuilder: (context, index) {

int currentIndex = (chatData.length - 1) - index;
bool isProcessing = processingData.containsKey(currentIndex) && processingData.containsKey(currentIndex);

return ChatBubble(
isUser: chatData[currentIndex]['role'] == 'user',
animateIcon: isProcessing,
fnCancelProcessing: isProcessing ? _cancelProcessing : null,
indexProcessing: (isProcessing && (processingData[currentIndex] != null && processingData[currentIndex]!)) ? currentIndex : null,
animateIcon: processingData.containsKey(currentIndex) && processingData[currentIndex]!,
fnChatActionCallback: _processChatAction,
index: currentIndex,
textData: chatData[currentIndex]['role'] == 'system' ? "!system_prompt_ignore" : chatData[currentIndex]['content'],
images: chatData[currentIndex]['images'] != null
? (chatData[currentIndex]['images'] as List<Map<String, String>>)
Expand Down Expand Up @@ -912,7 +910,7 @@ class CanvassState extends State<Canvass> {

}

Future<void> _sendPromptWithHistory() async {
Future<void> _sendPromptWithHistory({bool clearUserPrompt = true}) async {
if (!mounted) return;

final selectedModelProvider = Provider.of<SelectedModelProvider>(context, listen: false);
Expand All @@ -932,10 +930,12 @@ class CanvassState extends State<Canvass> {
);

setState(() {
// Clear files
base64Images.clear();
documents.clear();
codeFiles.clear();
if(clearUserPrompt){
// Clear files
base64Images.clear();
documents.clear();
codeFiles.clear();
}

// Add placeholder
chatData.add({'role': 'assistant', 'content': ''});
Expand Down Expand Up @@ -989,10 +989,45 @@ class CanvassState extends State<Canvass> {
);
}

void _cancelProcessing(int index){
if(processingData.containsKey(index)) {
processingData[index] = false;
void _processChatAction(ChatActionPayload payload) {

// Cancel
if( payload.actionType == ChatActionType.cancel)
{
if(processingData.containsKey(payload.index)) {
processingData[payload.index] = false;
}

return;
}

// Regenerate
if( payload.actionType == ChatActionType.regenerate)
{
setState(() {
// Remove the message
// todo: cache so user can switch back to prior responses
chatData.removeAt(payload.index);

});

// If this is(was) the last message - regenerate
if(payload.index == chatData.length)
{
_sendPromptWithHistory(clearUserPrompt: false);
}
return;
}

// Delete
if( payload.actionType == ChatActionType.delete)
{
setState(() {
chatData.removeAt(payload.index);
});
return;
}

}

bool _onChatStreamCancel(int index){
Expand Down Expand Up @@ -1322,20 +1357,10 @@ class ShiftEnterTextFormFieldState extends State<ShiftEnterTextFormField> {
onKeyEvent: (KeyEvent event) {
if (event is KeyDownEvent) {
if (HardwareKeyboard.instance.isShiftPressed && event.logicalKey == LogicalKeyboardKey.enter) {

// Insert a newline at the current cursor position
final currentText = widget.promptController.text;
final cursorPosition = widget.promptController.selection.baseOffset;
final newText = '${currentText.substring(0, cursorPosition)}\n${currentText.substring(cursorPosition)}';

setState(() {
widget.promptController.text = newText;
widget.promptController.selection = TextSelection.fromPosition(
TextPosition(offset: cursorPosition + 1),
);

});

// Capture shift-enter
} else if (HardwareKeyboard.instance.isControlPressed && event.logicalKey == LogicalKeyboardKey.enter) {
// Capture ctrl-enter
widget.promptController.text += '\n';
} else if (event.logicalKey == LogicalKeyboardKey.enter) {
sendPromptKeyEvent();
}
Expand Down
230 changes: 146 additions & 84 deletions confichat/lib/ui_widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -339,13 +339,35 @@ class CodePreviewBuilder extends MarkdownElementBuilder {

} // class CodePreviewBuilder

enum AiStatus {
complete,
processing
}

enum ChatActionType {
cancel,
delete,
regenerate
}

class ChatActionPayload {
final int index;
final ChatActionType actionType;

ChatActionPayload(
this.index,
this.actionType,
);
}


class ChatBubble extends StatelessWidget {
final String textData;
final bool isUser;
final bool animateIcon;

final Function(int)? fnCancelProcessing;
final int? indexProcessing;
final Function(ChatActionPayload) fnChatActionCallback;
final int index;
final List<String>? images;
final Iterable<String>? documents;
final Iterable<String>? codeFiles;
Expand All @@ -355,8 +377,8 @@ class ChatBubble extends StatelessWidget {
required this.isUser,
required this.animateIcon,
required this.textData,
this.fnCancelProcessing,
this.indexProcessing,
required this.index,
required this.fnChatActionCallback,
this.images,
this.documents,
this.codeFiles
Expand Down Expand Up @@ -406,104 +428,144 @@ class ChatBubble extends StatelessWidget {
const EdgeInsets.all(3) : const EdgeInsets.symmetric(horizontal: 3, vertical: 8).copyWith(right: 10),

child:
Row (
Stack (
children: [

// Icon
const SizedBox(width:10),
// Chat contents
Row( children: [
const SizedBox(width:10),

// Animated icon
if(!isUser && animateIcon) const AnimIconColorFade(
icon: Icons.psychology,
size: 24.0,
duration: 2
),
// Animated icon
if(!isUser && animateIcon) const AnimIconColorFade(
icon: Icons.psychology,
size: 24.0,
duration: 2
),

// Regular icon
if(!animateIcon) Icon(
isUser? Icons.person : Icons.psychology,
color: Colors.grey,
size: 24.0,
),
// Regular icon
if(!animateIcon) Icon(
isUser? Icons.person : Icons.psychology,
color: Colors.grey,
size: 24.0,
),

// Text
const SizedBox(width:20),
Expanded(
child: SelectionArea(
child: Container(
margin: const EdgeInsets.all(5),
child: Markdown(
data: sanitizedText,
builders: {
'code': CodePreviewBuilder(context),
},
shrinkWrap: true,
styleSheet: MarkdownStyleSheet(
h3: const TextStyle(color: Colors.black, fontSize: 18),
codeblockDecoration: BoxDecoration(
color: Colors.amber[100],
// Text
const SizedBox(width:20),
Expanded(
child: SelectionArea(
child: Container(
margin: const EdgeInsets.all(5),
child: isUser? Text(sanitizedText) : Markdown(
data: sanitizedText,
builders: {
'code': CodePreviewBuilder(context),
},
shrinkWrap: true,
styleSheet: MarkdownStyleSheet(
h3: const TextStyle(color: Colors.black, fontSize: 18),
codeblockDecoration: BoxDecoration(
color: Colors.amber[100],
),
),
),
)
)
)),
)
)
)),

// Images
if (images != null && images!.isNotEmpty)
Container(
constraints: BoxConstraints(
maxWidth: AppData.instance.getUserDeviceType(context) == UserDeviceType.phone ? 80 : 250,
),
child: Wrap(
spacing: 3.0,
children: images!.map((image) {
return GestureDetector(
onTap: () {
showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
child: ImagePreview(base64Image: image),
);
},
);
},
child: Container(
margin: const EdgeInsets.all(8),
child: Image.memory(
base64Decode(image),
height: 50,
width: 50,
fit: BoxFit.cover,
),
),
);
}).toList(),
),
),

]),

// Action icons

// Cancel
if (!isUser && animateIcon && indexProcessing != null)
Container(
if (!isUser && animateIcon)
Positioned(
right: 0,
bottom: 0,
child: Container(
constraints: const BoxConstraints(
maxWidth: 50,
),
child: IconButton(
icon: const Icon(Icons.cancel),
onPressed: indexProcessing == null ? null : () {
if (fnCancelProcessing != null && indexProcessing != null) {
fnCancelProcessing!(indexProcessing!);
}
}
),
child: IconButton(
icon: const Icon(Icons.cancel),
onPressed: () => fnChatActionCallback ( ChatActionPayload(index, ChatActionType.cancel) )
),
),
),

// Regenerate
if (!isUser && !animateIcon)
Positioned(
right: 0,
bottom: 0,
child: PopupMenuButton<String>(
tooltip: 'Remove/Regen',
iconColor: const Color.fromARGB(127, 158, 158, 158),
onSelected: (value) => fnChatActionCallback(ChatActionPayload(index, ChatActionType.regenerate)),
itemBuilder: (context) => [
const PopupMenuItem(
value: 'regenerate',
child: Text('Remove/Regenerate'),
),
],
)
),

// Images
if (images != null && images!.isNotEmpty)
Container(
constraints: BoxConstraints(
maxWidth: AppData.instance.getUserDeviceType(context) == UserDeviceType.phone ? 80 : 250,
),
child: Wrap(
spacing: 3.0,
children: images!.map((image) {
return GestureDetector(
onTap: () {
showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
child: ImagePreview(base64Image: image),
);
},
);
},
child: Container(
margin: const EdgeInsets.all(8),
child: Image.memory(
base64Decode(image),
height: 50,
width: 50,
fit: BoxFit.cover,
),
),
);
}).toList(),
),
),
// Delete
if (isUser)
Positioned(
right: 0,
bottom: 0,
child: PopupMenuButton<String>(
tooltip: 'Delete',
iconColor: const Color.fromARGB(127, 158, 158, 158),
onSelected: (value) => fnChatActionCallback(ChatActionPayload(index, ChatActionType.delete)),
itemBuilder: (context) => [
const PopupMenuItem(
value: 'delete',
child: Text('Delete'),
),
],
)
),

],
],
),

),
);

}

String removeAutoPrompt(String text, String delimeter) {
Expand Down

0 comments on commit 4b63f31

Please sign in to comment.