2025-08-20 17:01:46 +05:30
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
|
2025-10-04 16:04:49 +05:30
|
|
|
import 'package:markdown_widget/markdown_widget.dart';
|
|
|
|
|
|
2025-10-02 20:21:21 +05:30
|
|
|
import '../../theme/theme_extensions.dart';
|
2025-09-30 20:49:02 +05:30
|
|
|
import 'markdown_config.dart';
|
2025-10-02 20:21:21 +05:30
|
|
|
import 'markdown_preprocessor.dart';
|
2025-08-20 17:01:46 +05:30
|
|
|
|
2025-10-02 15:21:44 +05:30
|
|
|
class StreamingMarkdownWidget extends StatelessWidget {
|
2025-08-20 17:01:46 +05:30
|
|
|
const StreamingMarkdownWidget({
|
|
|
|
|
super.key,
|
2025-09-30 20:49:02 +05:30
|
|
|
required this.content,
|
2025-08-20 17:01:46 +05:30
|
|
|
required this.isStreaming,
|
2025-09-30 20:49:02 +05:30
|
|
|
this.onTapLink,
|
2025-08-20 17:01:46 +05:30
|
|
|
});
|
|
|
|
|
|
2025-09-30 20:49:02 +05:30
|
|
|
final String content;
|
|
|
|
|
final bool isStreaming;
|
2025-10-02 13:52:28 +05:30
|
|
|
final MarkdownLinkTapCallback? onTapLink;
|
2025-08-20 17:01:46 +05:30
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
2025-09-30 20:49:02 +05:30
|
|
|
if (content.trim().isEmpty) {
|
2025-10-02 20:21:21 +05:30
|
|
|
return const SizedBox.shrink();
|
2025-08-20 17:01:46 +05:30
|
|
|
}
|
|
|
|
|
|
2025-10-02 20:21:21 +05:30
|
|
|
final normalized = ConduitMarkdownPreprocessor.normalize(content);
|
2025-10-04 16:04:49 +05:30
|
|
|
final mermaidRegex = RegExp(r'```mermaid\s*([\s\S]*?)```', multiLine: true);
|
|
|
|
|
final matches = mermaidRegex.allMatches(normalized).toList();
|
|
|
|
|
final renderComponents = ConduitMarkdown.prepare(
|
2025-10-04 13:37:47 +05:30
|
|
|
context,
|
|
|
|
|
onTapLink: onTapLink,
|
|
|
|
|
);
|
2025-10-03 16:29:21 +05:30
|
|
|
|
2025-10-04 16:04:49 +05:30
|
|
|
Widget buildMarkdown(String data) {
|
|
|
|
|
return MarkdownBlock(
|
|
|
|
|
data: data,
|
|
|
|
|
selectable: false,
|
|
|
|
|
config: renderComponents.config,
|
|
|
|
|
generator: renderComponents.generator,
|
|
|
|
|
);
|
2025-10-04 13:37:47 +05:30
|
|
|
}
|
2025-10-03 13:37:57 +05:30
|
|
|
|
2025-10-03 16:29:21 +05:30
|
|
|
if (matches.isEmpty) {
|
|
|
|
|
return SelectionArea(
|
|
|
|
|
child: Theme(
|
|
|
|
|
data: Theme.of(context).copyWith(
|
|
|
|
|
textSelectionTheme: TextSelectionThemeData(
|
|
|
|
|
cursorColor: context.conduitTheme.buttonPrimary,
|
|
|
|
|
),
|
|
|
|
|
),
|
2025-10-04 16:04:49 +05:30
|
|
|
child: buildMarkdown(normalized),
|
2025-10-03 16:29:21 +05:30
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final children = <Widget>[];
|
|
|
|
|
var currentIndex = 0;
|
|
|
|
|
for (final match in matches) {
|
|
|
|
|
final before = normalized.substring(currentIndex, match.start);
|
|
|
|
|
if (before.trim().isNotEmpty) {
|
2025-10-04 16:04:49 +05:30
|
|
|
children.add(buildMarkdown(before));
|
2025-10-03 16:29:21 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final code = match.group(1)?.trim() ?? '';
|
|
|
|
|
if (code.isNotEmpty) {
|
2025-10-04 16:04:49 +05:30
|
|
|
children.add(ConduitMarkdown.buildMermaidBlock(context, code));
|
2025-10-03 16:29:21 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
currentIndex = match.end;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final tail = normalized.substring(currentIndex);
|
|
|
|
|
if (tail.trim().isNotEmpty) {
|
2025-10-04 16:04:49 +05:30
|
|
|
children.add(buildMarkdown(tail));
|
2025-10-03 16:29:21 +05:30
|
|
|
}
|
|
|
|
|
|
2025-10-03 14:53:50 +05:30
|
|
|
return SelectionArea(
|
2025-10-03 13:37:57 +05:30
|
|
|
child: Theme(
|
2025-10-03 14:53:50 +05:30
|
|
|
data: Theme.of(context).copyWith(
|
|
|
|
|
textSelectionTheme: TextSelectionThemeData(
|
|
|
|
|
cursorColor: context.conduitTheme.buttonPrimary,
|
2025-10-03 13:37:57 +05:30
|
|
|
),
|
2025-10-02 20:21:21 +05:30
|
|
|
),
|
2025-10-03 16:29:21 +05:30
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
|
|
|
children: children,
|
|
|
|
|
),
|
2025-10-02 13:52:28 +05:30
|
|
|
),
|
2025-09-30 19:53:19 +05:30
|
|
|
);
|
2025-08-20 17:01:46 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
extension StreamingMarkdownExtension on String {
|
2025-10-02 20:21:21 +05:30
|
|
|
Widget toMarkdown({
|
|
|
|
|
required BuildContext context,
|
|
|
|
|
bool isStreaming = false,
|
|
|
|
|
MarkdownLinkTapCallback? onTapLink,
|
|
|
|
|
}) {
|
|
|
|
|
return StreamingMarkdownWidget(
|
|
|
|
|
content: this,
|
|
|
|
|
isStreaming: isStreaming,
|
|
|
|
|
onTapLink: onTapLink,
|
|
|
|
|
);
|
2025-08-20 17:01:46 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class MarkdownWithLoading extends StatelessWidget {
|
2025-09-30 20:49:02 +05:30
|
|
|
const MarkdownWithLoading({super.key, this.content, required this.isLoading});
|
|
|
|
|
|
2025-08-20 17:01:46 +05:30
|
|
|
final String? content;
|
|
|
|
|
final bool isLoading;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
2025-09-30 20:49:02 +05:30
|
|
|
final value = content ?? '';
|
2025-10-02 20:21:21 +05:30
|
|
|
if (isLoading && value.trim().isEmpty) {
|
2025-09-30 16:02:34 +05:30
|
|
|
return const Center(child: CircularProgressIndicator());
|
2025-08-20 17:01:46 +05:30
|
|
|
}
|
|
|
|
|
|
2025-09-30 20:49:02 +05:30
|
|
|
return StreamingMarkdownWidget(content: value, isStreaming: isLoading);
|
2025-08-20 17:01:46 +05:30
|
|
|
}
|
2025-09-24 12:00:49 +05:30
|
|
|
}
|