diff --git a/assets/fonts/.gitignore b/assets/fonts/.gitignore deleted file mode 100644 index a4d2a77..0000000 --- a/assets/fonts/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:da96659fdd743ea8b8030a95e6a10279b21648a590d9d69f0531efc1c200b52f -size 32 diff --git a/assets/fonts/WeiNiZhuYiLangManXingShu-2.ttf b/assets/fonts/WeiNiZhuYiLangManXingShu-2.ttf new file mode 100644 index 0000000..343053f --- /dev/null +++ b/assets/fonts/WeiNiZhuYiLangManXingShu-2.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e6aaad752af70dcd9e1d50b98d608e67d40666f39c7725e2587a5655f47cd24 +size 22141520 diff --git a/assets/llm.json b/assets/llm.json new file mode 100644 index 0000000..562098a --- /dev/null +++ b/assets/llm.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a4d67a3a300aa2cfda52e61619405ab600e7cf09df9efdbde776da5ae0b33ad +size 636 diff --git a/assets/llm/banner.jpg b/assets/llm/banner.jpg new file mode 100644 index 0000000..6a1591d --- /dev/null +++ b/assets/llm/banner.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0cb4ab66896a5a1c1840ffd81a756baf743d755a2b1587c715816a81980ef3f1 +size 172921 diff --git a/assets/llm/chat.png b/assets/llm/chat.png new file mode 100644 index 0000000..3d8e9c8 --- /dev/null +++ b/assets/llm/chat.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:816a8685ff247ca55cd8bb0087f225cce477a41c27cf52c20e042b2e9147cdaf +size 1123 diff --git a/assets/llm/engineer.png b/assets/llm/engineer.png new file mode 100644 index 0000000..40c6cc8 --- /dev/null +++ b/assets/llm/engineer.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:658098dd2669caaff4e3a385371a885187a39c16a52b72cd626ab1c30c58312d +size 159749 diff --git a/assets/llm/zg.png b/assets/llm/zg.png new file mode 100644 index 0000000..90d1016 --- /dev/null +++ b/assets/llm/zg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a45df02716792538e821df5a3647a22983a9cc4bfb5e6c1587c7f5b47fe936b9 +size 149795 diff --git a/lib/layout/components/animated_sidebar.dart b/lib/layout/components/animated_sidebar.dart index 0e0c312..709fbfa 100644 --- a/lib/layout/components/animated_sidebar.dart +++ b/lib/layout/components/animated_sidebar.dart @@ -34,9 +34,7 @@ class _AnimatedSidebarState extends ConsumerState }); if (width == LayoutStyle.sidebarExpand) { ref.read(sidebarProvider.notifier).changeStatus(true); - } - - if (width == LayoutStyle.sidebarCollapse) { + } else { ref.read(sidebarProvider.notifier).changeStatus(false); } }); diff --git a/lib/layout/layout.dart b/lib/layout/layout.dart index 9b85750..fa8bf25 100644 --- a/lib/layout/layout.dart +++ b/lib/layout/layout.dart @@ -45,6 +45,7 @@ class _LayoutState extends ConsumerState with TickerProviderStateMixin { scale: _animation, child: PageView( controller: ref.read(pageProvider.notifier).pageController, + physics: const NeverScrollableScrollPhysics(), children: const [ WorkboardScreen(), EntryScreen(), diff --git a/lib/llm/chatchat/components/history_item_widget.dart b/lib/llm/chatchat/components/history_item_widget.dart index 5289b84..84b8e1c 100644 --- a/lib/llm/chatchat/components/history_item_widget.dart +++ b/lib/llm/chatchat/components/history_item_widget.dart @@ -7,8 +7,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'history_list.dart'; class HistoryList extends ConsumerWidget { - const HistoryList({super.key, required this.llmType}); + const HistoryList({super.key, required this.llmType, this.bottom}); final LLMType llmType; + final Widget? bottom; @override Widget build(BuildContext context, WidgetRef ref) { @@ -38,16 +39,7 @@ class HistoryList extends ConsumerWidget { llmType: llmType, ); })), - // const SizedBox( - // height: 35, - // child: Row( - // mainAxisAlignment: MainAxisAlignment.start, - // children: [ - // ChangeHostToolTipWidget(), - // ChangeChatTypeWidget() - // ], - // ), - // ) + bottom ?? const SizedBox() ], ), _ => const Center( diff --git a/lib/llm/chatchat/components/latex.dart b/lib/llm/chatchat/components/latex.dart new file mode 100644 index 0000000..7a5ef38 --- /dev/null +++ b/lib/llm/chatchat/components/latex.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:markdown_widget/markdown_widget.dart'; +// ignore: depend_on_referenced_packages +import 'package:markdown/markdown.dart' as m; +import 'package:flutter_math_fork/flutter_math.dart'; + +SpanNodeGeneratorWithTag latexGenerator = SpanNodeGeneratorWithTag( + tag: _latexTag, + generator: (e, config, visitor) => + LatexNode(e.attributes, e.textContent, config)); + +const _latexTag = 'latex'; + +class LatexSyntax extends m.InlineSyntax { + LatexSyntax() : super(r'(\$\$[\s\S]+\$\$)|(\$.+?\$)'); + + @override + bool onMatch(m.InlineParser parser, Match match) { + final input = match.input; + final matchValue = input.substring(match.start, match.end); + String content = ''; + bool isInline = true; + const blockSyntax = '\$\$'; + const inlineSyntax = '\$'; + if (matchValue.startsWith(blockSyntax) && + matchValue.endsWith(blockSyntax) && + (matchValue != blockSyntax)) { + content = matchValue.substring(2, matchValue.length - 2); + isInline = false; + } else if (matchValue.startsWith(inlineSyntax) && + matchValue.endsWith(inlineSyntax) && + matchValue != inlineSyntax) { + content = matchValue.substring(1, matchValue.length - 1); + } + m.Element el = m.Element.text(_latexTag, matchValue); + el.attributes['content'] = content; + el.attributes['isInline'] = '$isInline'; + parser.addNode(el); + return true; + } +} + +class LatexNode extends SpanNode { + final Map attributes; + final String textContent; + final MarkdownConfig config; + + LatexNode(this.attributes, this.textContent, this.config); + + @override + InlineSpan build() { + final content = attributes['content'] ?? ''; + final isInline = attributes['isInline'] == 'true'; + final style = parentStyle ?? config.p.textStyle; + if (content.isEmpty) return TextSpan(style: style, text: textContent); + final latex = Math.tex( + content, + mathStyle: MathStyle.text, + textStyle: style.copyWith(color: Colors.black), + textScaleFactor: 1, + onErrorFallback: (error) { + return Text( + textContent, + style: style.copyWith(color: Colors.red), + ); + }, + ); + return WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: !isInline + ? Container( + width: double.infinity, + margin: const EdgeInsets.symmetric(vertical: 16), + child: Center(child: latex), + ) + : latex); + } +} diff --git a/lib/llm/chatchat/models/response_message_box.dart b/lib/llm/chatchat/models/response_message_box.dart index cc5cb70..acd2e7f 100644 --- a/lib/llm/chatchat/models/response_message_box.dart +++ b/lib/llm/chatchat/models/response_message_box.dart @@ -2,6 +2,7 @@ import 'package:all_in_one/common/color_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; // import 'package:flutter_markdown/flutter_markdown.dart'; +import '../components/latex.dart'; import 'message_box.dart'; import 'package:markdown_widget/markdown_widget.dart'; @@ -34,7 +35,24 @@ class ResponseMessageBox extends MessageBox { children: [ Align( alignment: Alignment.topLeft, - child: MarkdownBlock(data: content), + child: MarkdownBlock( + data: content, + generator: MarkdownGenerator( + generators: [latexGenerator], + inlineSyntaxList: [LatexSyntax()], + richTextBuilder: (span) => + Text.rich(span, textScaler: const TextScaler.linear(1)), + ), + ), + // child: MarkdownWidget( + // data: content, + // markdownGenerator: MarkdownGenerator( + // generators: [latexGenerator], + // inlineSyntaxList: [LatexSyntax()], + // richTextBuilder: (span) => + // Text.rich(span, textScaler: const TextScaler.linear(1)), + // ), + // ), ), Material( color: Colors.transparent, diff --git a/lib/llm/langchain/components/buttons.dart b/lib/llm/langchain/components/buttons.dart new file mode 100644 index 0000000..89674f0 --- /dev/null +++ b/lib/llm/langchain/components/buttons.dart @@ -0,0 +1,59 @@ +import 'package:all_in_one/llm/langchain/notifiers/tool_notifier.dart'; +import 'package:all_in_one/styles/app_style.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class Buttons extends ConsumerWidget { + const Buttons({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return FittedBox( + child: Container( + height: 40, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: AppStyle.appColor, + border: Border.all(color: AppStyle.appColor), + boxShadow: const [ + BoxShadow( + color: AppStyle.appColor, + offset: Offset(0, 4), + blurRadius: 10, + spreadRadius: 1, + ) + ]), + child: Row( + children: [ + GestureDetector( + onTap: () { + ref.read(toolProvider.notifier).changeState(null); + }, + child: _wrapper(const Text( + "返回", + style: TextStyle(color: Colors.white), + )), + ), + const VerticalDivider(), + GestureDetector( + onTap: () {}, + child: _wrapper(const Text( + "Chains", + style: TextStyle(color: Colors.white), + )), + ) + ], + ), + ), + ); + } + + Widget _wrapper(Widget child) { + return SizedBox( + width: 60, + child: Center( + child: child, + ), + ); + } +} diff --git a/lib/llm/langchain/components/chat_ui.dart b/lib/llm/langchain/components/chat_ui.dart index 370a215..d7002c0 100644 --- a/lib/llm/langchain/components/chat_ui.dart +++ b/lib/llm/langchain/components/chat_ui.dart @@ -7,6 +7,7 @@ import 'package:all_in_one/llm/chatchat/notifiers/history_notifier.dart'; import 'package:all_in_one/llm/chatchat/notifiers/message_notifier.dart'; import 'package:all_in_one/llm/chatchat/notifiers/message_state.dart'; import 'package:all_in_one/llm/langchain/langchain_config.dart'; +import 'package:all_in_one/llm/langchain/notifiers/tool_notifier.dart'; import 'package:all_in_one/src/rust/api/llm_api.dart' as llm; import 'package:all_in_one/src/rust/llm.dart'; import 'package:flutter/material.dart'; @@ -121,6 +122,8 @@ class _ChatUIState extends ConsumerState { if (state.isLoading) { return; } + final sysPrompt = ref.read(toolProvider); + ref .read(messageProvider.notifier) .addMessageBox(RequestMessageBox(content: s)); @@ -134,19 +137,28 @@ class _ChatUIState extends ConsumerState { final messages = ref .read(historyProvider(LLMType.openai).notifier) .getMessages(config.historyLength, id); - for (final i in messages) { - logger.info("${i.roleType} ${i.content}"); + + List history; + if (sysPrompt != null && sysPrompt.content != "normal") { + history = [ + sysPrompt, + ...messages.map((e) => + LLMMessage(uuid: "", content: e.content ?? "", type: e.roleType)) + ]; + } else { + history = messages + .map((e) => + LLMMessage(uuid: "", content: e.content ?? "", type: e.roleType)) + .toList(); + } + + for (final i in history) { + logger.info("${i.type} ${i.content}"); } ref .read(historyProvider(LLMType.openai).notifier) .updateHistory(id, s, MessageType.query); - llm.chat( - stream: true, - query: s, - history: messages - .map((e) => LLMMessage( - uuid: "", content: e.content ?? "", type: e.roleType)) - .toList()); + llm.chat(stream: true, query: s, history: history); } } diff --git a/lib/llm/langchain/components/tools_item.dart b/lib/llm/langchain/components/tools_item.dart new file mode 100644 index 0000000..0751d85 --- /dev/null +++ b/lib/llm/langchain/components/tools_item.dart @@ -0,0 +1,69 @@ +import 'package:all_in_one/llm/langchain/models/tool_model.dart'; +import 'package:all_in_one/llm/langchain/notifiers/tool_notifier.dart'; +import 'package:all_in_one/styles/app_style.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class ToolsItem extends ConsumerStatefulWidget { + const ToolsItem({super.key, required this.toolModel}); + final ToolModel toolModel; + + @override + ConsumerState createState() => _ToolsItemState(); +} + +class _ToolsItemState extends ConsumerState { + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () { + ref + .read(toolProvider.notifier) + .changeState(widget.toolModel.toMessage()); + }, + child: FittedBox( + child: Stack( + children: [ + const SizedBox( + height: 70, + width: 200, + ), + Positioned( + bottom: 0, + child: Container( + width: 200, + height: 50, + padding: const EdgeInsets.only(left: 30), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: Colors.white, + border: Border.all(color: AppStyle.appColor), + boxShadow: const [ + BoxShadow( + color: AppStyle.appColor, + offset: Offset(0, 4), + blurRadius: 10, + spreadRadius: 3, + ) + ]), + child: Center( + child: Text( + widget.toolModel.name, + style: const TextStyle(fontFamily: "xing", fontSize: 20), + ), + ), + )), + Positioned( + top: 0, + left: 10, + child: SizedBox( + width: 50, + height: 50, + child: Image.asset(widget.toolModel.imgPath), + )), + ], + ), + ), + ); + } +} diff --git a/lib/llm/langchain/components/tools_screen.dart b/lib/llm/langchain/components/tools_screen.dart new file mode 100644 index 0000000..1f64274 --- /dev/null +++ b/lib/llm/langchain/components/tools_screen.dart @@ -0,0 +1,95 @@ +import 'dart:convert'; + +import 'package:all_in_one/llm/langchain/models/tool_model.dart'; +import 'package:all_in_one/styles/app_style.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'tools_item.dart'; + +class ToolsScreen extends StatefulWidget { + const ToolsScreen({super.key}); + + @override + State createState() => _ToolsScreenState(); +} + +class _ToolsScreenState extends State { + @override + void initState() { + super.initState(); + future = loadData(); + } + + // ignore: prefer_typing_uninitialized_variables + var future; + + String textContent = ""; + loadData() async { + textContent = await rootBundle.loadString('assets/llm.json'); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(10), + child: FutureBuilder( + future: future, + builder: (c, s) { + if (s.connectionState == ConnectionState.done) { + final Map jsonObj = json.decode(textContent); + return Column( + children: [ + Stack( + children: [ + Container( + width: double.infinity, + constraints: const BoxConstraints(maxHeight: 400), + child: Image.asset( + "assets/llm/banner.jpg", + fit: BoxFit.cover, + ), + ), + Positioned( + bottom: 40, + right: 20, + child: Transform.rotate( + angle: -3.14 / 10, + child: const Text( + "Let AI help you", + style: TextStyle( + fontFamily: "xing", + fontSize: 40, + color: AppStyle.orange), + ), + )) + ], + ), + const SizedBox( + height: 10, + ), + Expanded( + child: Align( + alignment: Alignment.topLeft, + child: SingleChildScrollView( + child: Wrap( + runSpacing: 15, + spacing: 15, + children: jsonObj.entries + .map((e) => ToolsItem( + toolModel: ToolModel.fromJson(e.value), + )) + .toList(), + ), + ), + )) + ], + ); + } + return const Center( + child: CircularProgressIndicator(), + ); + }), + ); + } +} diff --git a/lib/llm/langchain/langchain_chat_screen.dart b/lib/llm/langchain/langchain_chat_screen.dart index 63013aa..c372274 100644 --- a/lib/llm/langchain/langchain_chat_screen.dart +++ b/lib/llm/langchain/langchain_chat_screen.dart @@ -1,27 +1,46 @@ import 'package:all_in_one/isar/llm_history.dart'; import 'package:all_in_one/llm/chatchat/components/history_item_widget.dart'; +import 'package:all_in_one/llm/langchain/components/buttons.dart'; import 'package:all_in_one/llm/langchain/components/chat_ui.dart'; +import 'package:all_in_one/llm/langchain/components/tools_screen.dart'; +import 'package:all_in_one/llm/langchain/notifiers/tool_notifier.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; -class LangchainChatScreen extends StatefulWidget { +class LangchainChatScreen extends ConsumerStatefulWidget { const LangchainChatScreen({super.key}); @override - State createState() => _LangchainChatScreenState(); + ConsumerState createState() => + _LangchainChatScreenState(); } -class _LangchainChatScreenState extends State { +class _LangchainChatScreenState extends ConsumerState { @override Widget build(BuildContext context) { - return const Scaffold( - body: Row( - children: [ - HistoryList( - llmType: LLMType.openai, - ), - Expanded(child: ChatUI()) - ], + return Scaffold( + body: PageView( + physics: const NeverScrollableScrollPhysics(), + controller: ref.read(toolProvider.notifier).controller, + children: const [ToolsScreen(), _UI()], ), ); } } + +class _UI extends StatelessWidget { + const _UI(); + + @override + Widget build(BuildContext context) { + return const Row( + children: [ + HistoryList( + llmType: LLMType.openai, + bottom: Buttons(), + ), + Expanded(child: ChatUI()) + ], + ); + } +} diff --git a/lib/llm/langchain/models/tool_model.dart b/lib/llm/langchain/models/tool_model.dart new file mode 100644 index 0000000..6e39654 --- /dev/null +++ b/lib/llm/langchain/models/tool_model.dart @@ -0,0 +1,21 @@ +import 'package:all_in_one/src/rust/llm.dart'; + +class ToolModel { + final String imgPath; + final String systemPrompt; + final String name; + + ToolModel( + {required this.imgPath, required this.systemPrompt, required this.name}); + + factory ToolModel.fromJson(Map json) { + return ToolModel( + imgPath: json['img_path'], + systemPrompt: json['system_prompt'], + name: json['name']); + } + + LLMMessage toMessage() { + return LLMMessage(uuid: "", content: systemPrompt, type: 1); + } +} diff --git a/lib/llm/langchain/notifiers/tool_notifier.dart b/lib/llm/langchain/notifiers/tool_notifier.dart new file mode 100644 index 0000000..b3076cf --- /dev/null +++ b/lib/llm/langchain/notifiers/tool_notifier.dart @@ -0,0 +1,26 @@ +import 'package:all_in_one/src/rust/llm.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class ToolNotifier extends Notifier { + final PageController controller = PageController(); + + @override + LLMMessage? build() { + return null; + } + + changeState(LLMMessage? message) { + state = message; + + if (message == null) { + controller.jumpToPage(0); + } else { + controller.jumpToPage(1); + } + } +} + +final toolProvider = NotifierProvider( + () => ToolNotifier(), +); diff --git a/pubspec.lock b/pubspec.lock index 65fa4b4..eb945be 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -460,6 +460,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" + flutter_math_fork: + dependency: "direct main" + description: + name: flutter_math_fork + sha256: "94bee4642892a94939af0748c6a7de0ff8318feee588379dcdfea7dc5cba06c8" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.2" flutter_riverpod: dependency: "direct main" description: @@ -1149,6 +1157,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.2.1" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.2" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index daca3d1..57dff67 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,6 +27,7 @@ dependencies: git: url: https://github.com/guchengxi1994/flutter_context_menu flutter_layout_grid: ^2.0.6 + flutter_math_fork: ^0.7.2 flutter_riverpod: ^2.5.1 flutter_rust_bridge: 2.0.0-dev.31 gauge_indicator: ^0.4.3 @@ -82,11 +83,16 @@ flutter: - assets/icon.ico - assets/icon.png - assets/icons/ + - assets/llm.json + - assets/llm/ fonts: - family: NotoSns fonts: - asset: assets/fonts/NotoSansSC-Bold.ttf + - family: xing + fonts: + - asset: assets/fonts/WeiNiZhuYiLangManXingShu-2.ttf flutter_launcher_icons: # flutter pub run flutter_launcher_icons android: "launcher_icon"