Feat: Clear Markup for code responses

This commit is contained in:
siddu015
2025-02-27 22:18:03 +05:30
parent 3fae321e3e
commit 1abfb4be2c
5 changed files with 167 additions and 173 deletions

View File

@ -0,0 +1,28 @@
// lib/dashbot/widgets/chat_bubble.dart
import 'package:flutter/material.dart';
import 'content_renderer.dart';
class ChatBubble extends StatelessWidget {
final String message;
final bool isUser;
const ChatBubble({super.key, required this.message, this.isUser = false});
@override
Widget build(BuildContext context) {
return Align(
alignment: isUser ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isUser
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: renderContent(context, message),
),
);
}
}

View File

@ -0,0 +1,97 @@
// lib/dashbot/widgets/content_renderer.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_highlighter/flutter_highlighter.dart';
import 'package:flutter_highlighter/themes/monokai-sublime.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
Widget renderContent(BuildContext context, String text) {
if (text.isEmpty) {
return const Text("No content to display.");
}
final codeBlockPattern = RegExp(r'```(\w+)?\n([\s\S]*?)```', multiLine: true);
final matches = codeBlockPattern.allMatches(text);
if (matches.isEmpty) {
return _renderMarkdown(context, text);
}
List<Widget> children = [];
int lastEnd = 0;
for (var match in matches) {
if (match.start > lastEnd) {
children.add(_renderMarkdown(context, text.substring(lastEnd, match.start)));
}
final language = match.group(1) ?? 'text';
final code = match.group(2)!.trim();
children.add(_renderCodeBlock(context, language, code));
lastEnd = match.end;
}
if (lastEnd < text.length) {
children.add(_renderMarkdown(context, text.substring(lastEnd)));
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
);
}
Widget _renderMarkdown(BuildContext context, String markdown) {
return MarkdownBody(
data: markdown,
selectable: true,
styleSheet: MarkdownStyleSheet(
p: TextStyle(color: Theme.of(context).colorScheme.onSurface),
),
);
}
Widget _renderCodeBlock(BuildContext context, String language, String code) {
if (language == 'json') {
try {
final prettyJson = const JsonEncoder.withIndent(' ').convert(jsonDecode(code));
return Container(
padding: const EdgeInsets.all(8),
color: Theme.of(context).colorScheme.surfaceContainerLow,
child: SelectableText(
prettyJson,
style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
),
);
} catch (e) {
return _renderFallbackCode(context, code);
}
} else {
try {
return Container(
padding: const EdgeInsets.all(8),
color: Theme.of(context).colorScheme.surfaceContainerLow,
child: HighlightView(
code,
language: language,
theme: monokaiSublimeTheme,
textStyle: const TextStyle(fontFamily: 'monospace', fontSize: 12),
),
);
} catch (e) {
return _renderFallbackCode(context, code);
}
}
}
Widget _renderFallbackCode(BuildContext context, String code) {
return Container(
padding: const EdgeInsets.all(8),
color: Theme.of(context).colorScheme.surfaceContainerLow,
child: SelectableText(
code,
style: const TextStyle(fontFamily: 'monospace', fontSize: 12, color: Colors.red),
),
);
}

View File

@ -1,12 +1,10 @@
// lib/dashbot/widgets/dashbot_widget.dart
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:apidash/dashbot/providers/dashbot_providers.dart'; import 'package:apidash/dashbot/providers/dashbot_providers.dart';
import 'package:apidash/providers/providers.dart'; import 'package:apidash/providers/providers.dart';
import 'package:highlighter/highlighter.dart' show highlight; import 'package:flutter/services.dart';
import 'package:json_explorer/json_explorer.dart'; import 'chat_bubble.dart';
import 'package:share_plus/share_plus.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'dart:convert';
class DashBotWidget extends ConsumerStatefulWidget { class DashBotWidget extends ConsumerStatefulWidget {
const DashBotWidget({Key? key}) : super(key: key); const DashBotWidget({Key? key}) : super(key: key);
@ -17,8 +15,22 @@ class DashBotWidget extends ConsumerStatefulWidget {
class _DashBotWidgetState extends ConsumerState<DashBotWidget> { class _DashBotWidgetState extends ConsumerState<DashBotWidget> {
final TextEditingController _controller = TextEditingController(); final TextEditingController _controller = TextEditingController();
late ScrollController _scrollController;
bool _isLoading = false; bool _isLoading = false;
@override
void initState() {
super.initState();
_scrollController = ScrollController();
}
@override
void dispose() {
_scrollController.dispose();
_controller.dispose();
super.dispose();
}
Future<void> _sendMessage(String message) async { Future<void> _sendMessage(String message) async {
if (message.trim().isEmpty) return; if (message.trim().isEmpty) return;
final dashBotService = ref.read(dashBotServiceProvider); final dashBotService = ref.read(dashBotServiceProvider);
@ -34,26 +46,29 @@ class _DashBotWidgetState extends ConsumerState<DashBotWidget> {
try { try {
final response = await dashBotService.handleRequest(message, requestModel, responseModel); final response = await dashBotService.handleRequest(message, requestModel, responseModel);
final formattedResponse = _limitResponse(response);
ref.read(chatMessagesProvider.notifier).addMessage({ ref.read(chatMessagesProvider.notifier).addMessage({
'role': 'bot', 'role': 'bot',
'message': formattedResponse, 'message': response,
}); });
} catch (error) { } catch (error, stackTrace) {
print('Error in _sendMessage: $error');
print('StackTrace: $stackTrace');
ref.read(chatMessagesProvider.notifier).addMessage({ ref.read(chatMessagesProvider.notifier).addMessage({
'role': 'bot', 'role': 'bot',
'message': "Error: ${error.toString()}", 'message': "Error: ${error.toString()}",
}); });
} finally { } finally {
setState(() => _isLoading = false); setState(() => _isLoading = false);
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollController.animateTo(
0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
});
} }
} }
String _limitResponse(String response) {
return response;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final messages = ref.watch(chatMessagesProvider); final messages = ref.watch(chatMessagesProvider);
@ -101,7 +116,10 @@ class _DashBotWidgetState extends ConsumerState<DashBotWidget> {
(msg) => msg['role'] == 'bot', (msg) => msg['role'] == 'bot',
orElse: () => {'message': ''}, orElse: () => {'message': ''},
)['message']; )['message'];
Share.share(lastBotMessage); Clipboard.setData(ClipboardData(text: lastBotMessage));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Copied to clipboard')),
);
}, },
), ),
IconButton( IconButton(
@ -143,6 +161,7 @@ class _DashBotWidgetState extends ConsumerState<DashBotWidget> {
Widget _buildChatArea(List<Map<String, dynamic>> messages) { Widget _buildChatArea(List<Map<String, dynamic>> messages) {
return ListView.builder( return ListView.builder(
controller: _scrollController,
reverse: true, reverse: true,
itemCount: messages.length, itemCount: messages.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
@ -191,162 +210,3 @@ class _DashBotWidgetState extends ConsumerState<DashBotWidget> {
); );
} }
} }
class ChatBubble extends StatelessWidget {
final String message;
final bool isUser;
const ChatBubble({super.key, required this.message, this.isUser = false});
@override
Widget build(BuildContext context) {
return Align(
alignment: isUser ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isUser
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: _renderContent(context, message),
),
);
}
Widget _renderContent(BuildContext context, String text) {
final codeBlockPattern = RegExp(r'```(\w+)?\n([\s\S]*?)```', multiLine: true);
final matches = codeBlockPattern.allMatches(text);
if (matches.isEmpty) {
return MarkdownBody(
data: text,
selectable: true,
styleSheet: MarkdownStyleSheet(
h1: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onSurface),
h2: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onSurface),
h3: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.onSurface),
strong: TextStyle(fontWeight: FontWeight.bold),
em: TextStyle(fontStyle: FontStyle.italic),
listBullet: TextStyle(color: Theme.of(context).colorScheme.onSurface),
p: TextStyle(color: Theme.of(context).colorScheme.onSurface),
),
);
}
List<Widget> children = [];
int lastEnd = 0;
for (var match in matches) {
if (match.start > lastEnd) {
children.add(
MarkdownBody(
data: text.substring(lastEnd, match.start),
selectable: true,
styleSheet: MarkdownStyleSheet(
h1: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onSurface),
h2: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onSurface),
h3: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.onSurface),
strong: TextStyle(fontWeight: FontWeight.bold),
em: TextStyle(fontStyle: FontStyle.italic),
listBullet: TextStyle(color: Theme.of(context).colorScheme.onSurface),
p: TextStyle(color: Theme.of(context).colorScheme.onSurface),
),
),
);
}
final language = match.group(1) ?? 'text';
final code = match.group(2)!.trim();
if (language == 'json' && _isValidJson(code)) {
final prettyJson = const JsonEncoder.withIndent(' ').convert(jsonDecode(code));
children.add(
Container(
padding: const EdgeInsets.all(8),
color: Theme.of(context).colorScheme.surfaceContainerLow,
child: SelectableText(
prettyJson,
style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
),
),
);
} else {
final highlighted = highlight.parse(code, language: language);
final spans = _buildTextSpans(highlighted, context);
children.add(
Container(
padding: const EdgeInsets.all(8),
color: Theme.of(context).colorScheme.surfaceContainerLow,
child: SelectableText.rich(
TextSpan(children: spans),
style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
),
),
);
}
lastEnd = match.end;
}
if (lastEnd < text.length) {
children.add(
MarkdownBody(
data: text.substring(lastEnd),
selectable: true,
styleSheet: MarkdownStyleSheet(
h1: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onSurface),
h2: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onSurface),
h3: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.onSurface),
strong: TextStyle(fontWeight: FontWeight.bold),
em: TextStyle(fontStyle: FontStyle.italic),
listBullet: TextStyle(color: Theme.of(context).colorScheme.onSurface),
p: TextStyle(color: Theme.of(context).colorScheme.onSurface),
),
),
);
}
return Column(
crossAxisAlignment: isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: children,
);
}
bool _isValidJson(String text) {
try {
jsonDecode(text);
return true;
} catch (_) {
return false;
}
}
List<TextSpan> _buildTextSpans(dynamic highlighted, BuildContext context) {
final List<TextSpan> spans = [];
for (var span in highlighted.spans ?? []) {
spans.add(TextSpan(
text: span.text,
style: TextStyle(
color: _getColorForClassName(span.className, context),
),
));
}
return spans.isEmpty ? [TextSpan(text: highlighted.source)] : spans;
}
Color _getColorForClassName(String? className, BuildContext context) {
switch (className) {
case 'keyword':
return Colors.blue;
case 'string':
return Colors.green;
case 'comment':
return Colors.grey;
default:
return Theme.of(context).colorScheme.onSurface;
}
}
}

View File

@ -509,6 +509,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_highlighter:
dependency: "direct main"
description:
name: flutter_highlighter
sha256: "93173afd47a9ada53f3176371755e7ea4a1065362763976d06d6adfb4d946e10"
url: "https://pub.dev"
source: hosted
version: "0.1.1"
flutter_hooks: flutter_hooks:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -67,6 +67,7 @@ dependencies:
path: plugins/window_size path: plugins/window_size
share_plus: ^10.1.4 share_plus: ^10.1.4
ollama_dart: ^0.2.2 ollama_dart: ^0.2.2
flutter_highlighter: ^0.1.0
dependency_overrides: dependency_overrides:
extended_text_field: ^16.0.0 extended_text_field: ^16.0.0