Skip to content

Commit fbdc499

Browse files
committed
up
1 parent b0200fa commit fbdc499

17 files changed

+848
-267
lines changed

lib/main.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,20 @@ import './page/layout/layout.dart';
66
import './provider/provider_manager.dart';
77
import 'package:logging/logging.dart';
88
import 'package:window_manager_plus/window_manager_plus.dart';
9+
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
10+
import 'dart:io';
911

1012
void main() async {
1113
WidgetsFlutterBinding.ensureInitialized();
1214

1315
// 初始化窗口管理器
1416
await WindowManagerPlus.ensureInitialized(0);
1517

18+
// 初始化 InAppWebView
19+
if (Platform.isAndroid) {
20+
await InAppWebViewController.setWebContentsDebuggingEnabled(true);
21+
}
22+
1623
// 设置窗口选项
1724
WindowOptions windowOptions = const WindowOptions(
1825
size: Size(1200, 800),

lib/page/layout/chat_page/chat_message.dart

Lines changed: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
22
import 'package:ChatMcp/llm/model.dart';
33
import 'dart:convert';
44
import 'package:ChatMcp/widgets/collapsible_section.dart';
5-
import 'package:ChatMcp/widgets/markit.dart';
5+
import 'package:ChatMcp/widgets/markdown/markit.dart';
66

77
class ChatUIMessage extends StatelessWidget {
88
final List<ChatMessage> messages;
@@ -74,52 +74,58 @@ class ChatUIMessage extends StatelessWidget {
7474
child: msg.role == MessageRole.user
7575
? TextSelectionTheme(
7676
data: TextSelectionThemeData(
77-
selectionColor: Colors.white.withOpacity(0.3),
77+
selectionColor: Colors.white.withAlpha(77),
7878
),
7979
child: SelectableText(
8080
msg.content ?? '',
8181
style: const TextStyle(color: Colors.white),
8282
),
8383
)
8484
: msg.content != null
85-
? Markit(data: msg.content!)
85+
? Markit(data: (msg.content!).trim())
8686
: const Text(''),
8787
),
8888
if (msg.toolCalls != null && msg.toolCalls!.isNotEmpty)
89-
CollapsibleSection(
90-
title: Text(
91-
'${msg.mcpServerName} call_${msg.toolCalls![0]['function']['name']}',
92-
style: TextStyle(
93-
fontSize: 12,
94-
color: Colors.grey[600],
95-
fontStyle: FontStyle.italic,
89+
Container(
90+
margin: const EdgeInsets.symmetric(vertical: 8.0),
91+
child: CollapsibleSection(
92+
title: Text(
93+
'${msg.mcpServerName} call_${msg.toolCalls![0]['function']['name']}',
94+
style: TextStyle(
95+
fontSize: 12,
96+
color: Colors.grey[600],
97+
fontStyle: FontStyle.italic,
98+
),
99+
),
100+
content: Markit(
101+
data: (msg.toolCalls?.isNotEmpty ?? false)
102+
? [
103+
'```json',
104+
const JsonEncoder.withIndent(' ').convert({
105+
"name": msg.toolCalls![0]['function']['name'],
106+
"arguments": json.decode(
107+
msg.toolCalls![0]['function']['arguments']),
108+
}),
109+
'```',
110+
].join('\n')
111+
: '',
96112
),
97-
),
98-
content: Markit(
99-
data: (msg.toolCalls?.isNotEmpty ?? false)
100-
? [
101-
'```json',
102-
const JsonEncoder.withIndent(' ').convert({
103-
"name": msg.toolCalls![0]['function']['name'],
104-
"arguments": json
105-
.decode(msg.toolCalls![0]['function']['arguments']),
106-
}),
107-
'```',
108-
].join('\n')
109-
: '',
110113
),
111114
),
112115
if (msg.role == MessageRole.tool && msg.toolCallId != null)
113-
CollapsibleSection(
114-
title: Text(
115-
'${msg.mcpServerName} ${msg.toolCallId!} result',
116-
style: TextStyle(
117-
fontSize: 12,
118-
color: Colors.grey[600],
119-
fontStyle: FontStyle.italic,
116+
Container(
117+
margin: const EdgeInsets.symmetric(vertical: 8.0),
118+
child: CollapsibleSection(
119+
title: Text(
120+
'${msg.mcpServerName} ${msg.toolCallId!} result',
121+
style: TextStyle(
122+
fontSize: 12,
123+
color: Colors.grey[600],
124+
fontStyle: FontStyle.italic,
125+
),
120126
),
127+
content: Markit(data: (msg.content ?? '').trim()),
121128
),
122-
content: Markit(data: msg.content ?? ''),
123129
),
124130
],
125131
);

lib/widgets/collapsible_section.dart

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,31 +21,35 @@ class _CollapsibleSectionState extends State<CollapsibleSection> {
2121

2222
@override
2323
Widget build(BuildContext context) {
24-
return Column(
25-
crossAxisAlignment: CrossAxisAlignment.start,
26-
children: [
27-
InkWell(
28-
onTap: () => setState(() => _isExpanded = !_isExpanded),
29-
child: Row(
30-
children: [
31-
Icon(
32-
_isExpanded
33-
? Icons.keyboard_arrow_down
34-
: Icons.keyboard_arrow_right,
35-
size: 16,
36-
color: Colors.grey[600],
37-
),
38-
Expanded(child: widget.title),
39-
],
24+
return Container(
25+
width: double.infinity,
26+
child: Column(
27+
mainAxisSize: MainAxisSize.min,
28+
crossAxisAlignment: CrossAxisAlignment.start,
29+
children: [
30+
InkWell(
31+
onTap: () => setState(() => _isExpanded = !_isExpanded),
32+
child: Row(
33+
children: [
34+
Icon(
35+
_isExpanded
36+
? Icons.keyboard_arrow_down
37+
: Icons.keyboard_arrow_right,
38+
size: 16,
39+
color: Colors.grey[600],
40+
),
41+
Expanded(child: widget.title),
42+
],
43+
),
4044
),
41-
),
42-
if (_isExpanded)
43-
Padding(
44-
padding:
45-
widget.padding ?? const EdgeInsets.only(top: 4.0, left: 8.0),
46-
child: widget.content,
47-
),
48-
],
45+
if (_isExpanded)
46+
Padding(
47+
padding:
48+
widget.padding ?? const EdgeInsets.only(top: 4.0, left: 8.0),
49+
child: widget.content,
50+
),
51+
],
52+
),
4953
);
5054
}
5155
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_markdown/flutter_markdown.dart';
3+
import 'package:flutter_highlighter/themes/github.dart';
4+
import 'package:markdown/markdown.dart' as md;
5+
6+
import '../widgets/highlight_view.dart';
7+
import '../widgets/mermaid_diagram_view.dart' show MermaidDiagramView;
8+
import '../widgets/html_view.dart';
9+
10+
class CodeElementBuilder extends MarkdownElementBuilder {
11+
@override
12+
Widget? visitElementAfter(md.Element element, TextStyle? preferredStyle) {
13+
var language = '';
14+
if (element.attributes['class'] != null) {
15+
String lg = element.attributes['class'] as String;
16+
if (lg.startsWith('language-')) {
17+
language = lg.substring(9);
18+
} else {
19+
language = lg;
20+
}
21+
}
22+
23+
final bool isInline = element.attributes['class'] == null;
24+
25+
if (isInline) {
26+
return Container(
27+
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
28+
decoration: BoxDecoration(
29+
color: Colors.grey[200],
30+
borderRadius: BorderRadius.circular(4),
31+
),
32+
child: Text(
33+
element.textContent,
34+
style: const TextStyle(
35+
fontFamily: 'monospace',
36+
fontSize: 14,
37+
),
38+
),
39+
);
40+
}
41+
42+
return _CodeBlock(
43+
code: element.textContent.trim(),
44+
language: language,
45+
);
46+
}
47+
}
48+
49+
class _CodeBlock extends StatefulWidget {
50+
final String code;
51+
final String language;
52+
53+
const _CodeBlock({
54+
required this.code,
55+
required this.language,
56+
});
57+
58+
@override
59+
State<_CodeBlock> createState() => _CodeBlockState();
60+
}
61+
62+
class _CodeBlockState extends State<_CodeBlock>
63+
with AutomaticKeepAliveClientMixin {
64+
bool _isPreviewVisible = false;
65+
66+
@override
67+
bool get wantKeepAlive => true;
68+
69+
@override
70+
Widget build(BuildContext context) {
71+
super.build(context);
72+
Widget previewWidget;
73+
74+
if (widget.language == 'mermaid' &&
75+
(widget.code.contains('sequenceDiagram') ||
76+
widget.code.contains('flowchart') ||
77+
widget.code.contains('classDiagram') ||
78+
widget.code.contains('stateDiagram') ||
79+
widget.code.contains('gantt') ||
80+
widget.code.contains('pie') ||
81+
widget.code.contains('erDiagram') ||
82+
widget.code.contains('journey'))) {
83+
previewWidget = MermaidDiagramView(code: widget.code);
84+
} else if (widget.language == 'html') {
85+
previewWidget = HtmlView(html: widget.code);
86+
} else {
87+
previewWidget = HighlightView(
88+
widget.code,
89+
language: widget.language,
90+
theme: githubTheme,
91+
padding: const EdgeInsets.all(8),
92+
textStyle: const TextStyle(
93+
fontFamily: 'monospace',
94+
fontSize: 14,
95+
),
96+
);
97+
}
98+
99+
return Container(
100+
width: double.infinity,
101+
decoration: BoxDecoration(
102+
color: Colors.grey[200],
103+
borderRadius: BorderRadius.circular(8),
104+
),
105+
child: Stack(
106+
children: [
107+
Padding(
108+
padding: const EdgeInsets.all(8.0),
109+
child: _isPreviewVisible
110+
? previewWidget
111+
: Text(
112+
widget.code,
113+
style: const TextStyle(
114+
fontFamily: 'monospace',
115+
fontSize: 14,
116+
),
117+
),
118+
),
119+
Positioned(
120+
right: 8,
121+
top: 8,
122+
child: TextButton(
123+
onPressed: () {
124+
setState(() {
125+
_isPreviewVisible = !_isPreviewVisible;
126+
});
127+
},
128+
style: TextButton.styleFrom(
129+
backgroundColor: Colors.white.withAlpha(204),
130+
padding:
131+
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
132+
shape: RoundedRectangleBorder(
133+
borderRadius: BorderRadius.circular(8),
134+
),
135+
),
136+
child: Text(
137+
_isPreviewVisible ? 'Code' : 'Preview',
138+
style: const TextStyle(fontSize: 12),
139+
),
140+
),
141+
),
142+
],
143+
),
144+
);
145+
}
146+
}

0 commit comments

Comments
 (0)