├── .github └── workflows │ └── build.yaml ├── .gitignore ├── .metadata ├── CHANGELOG ├── COPYING ├── README ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── cc │ │ │ │ └── arthur63 │ │ │ │ └── chatbot │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets └── images │ ├── claude.svg │ ├── deepseek.svg │ ├── gemini.svg │ ├── grok.svg │ ├── ollama.svg │ ├── openai.svg │ └── qwen.svg ├── lib ├── chat │ ├── chat.dart │ ├── current.dart │ ├── input.dart │ ├── message.dart │ └── settings.dart ├── config.dart ├── gen │ ├── intl │ │ ├── messages_all.dart │ │ ├── messages_en.dart │ │ └── messages_zh_CN.dart │ └── l10n.dart ├── image │ ├── config.dart │ ├── generate.dart │ └── image.dart ├── l10n │ ├── intl_en.arb │ └── intl_zh_CN.arb ├── llm │ ├── llm.dart │ └── web.dart ├── main.dart ├── markdown │ ├── code.dart │ ├── latex.dart │ └── util.dart ├── settings │ ├── api.dart │ ├── bot.dart │ ├── config.dart │ └── settings.dart ├── util.dart └── workspace │ ├── document.dart │ ├── model.dart │ ├── task.dart │ └── workspace.dart ├── linux ├── .gitignore ├── CMakeLists.txt ├── flutter │ ├── CMakeLists.txt │ ├── generated_plugin_registrant.cc │ ├── generated_plugin_registrant.h │ └── generated_plugins.cmake ├── main.cc ├── my_application.cc └── my_application.h ├── pubspec.lock ├── pubspec.yaml └── windows ├── .gitignore ├── CMakeLists.txt ├── flutter ├── CMakeLists.txt ├── generated_plugin_registrant.cc ├── generated_plugin_registrant.h └── generated_plugins.cmake └── runner ├── CMakeLists.txt ├── Runner.rc ├── flutter_window.cpp ├── flutter_window.h ├── main.cpp ├── resource.h ├── resources └── app_icon.ico ├── runner.exe.manifest ├── utils.cpp ├── utils.h ├── win32_window.cpp └── win32_window.h /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | build-apk: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Clone repository 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Setup JDK17 21 | uses: actions/setup-java@v4 22 | with: 23 | distribution: "zulu" 24 | java-version: "17" 25 | 26 | - name: Setup Flutter 27 | uses: subosito/flutter-action@v2 28 | with: 29 | flutter-version: 3.24.5 30 | 31 | - name: Setup signing 32 | run: | 33 | echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/app/keystore.jks 34 | echo "storePassword=${{ secrets.KEYSTORE_PASSWORD }}" > android/key.properties 35 | echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/key.properties 36 | echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/key.properties 37 | echo "storeFile=keystore.jks" >> android/key.properties 38 | 39 | - name: Build 40 | run: | 41 | flutter pub get 42 | flutter build apk --split-per-abi 43 | 44 | - name: ChangeLog 45 | id: changelog 46 | run: | 47 | echo "latest<> $GITHUB_OUTPUT 48 | awk '/^[0-9]/{i++}i==1' CHANGELOG | sed '${/^$/d}' >> $GITHUB_OUTPUT 49 | echo "EOF" >> $GITHUB_OUTPUT 50 | 51 | - name: Release 52 | uses: softprops/action-gh-release@v2 53 | with: 54 | files: | 55 | build/app/outputs/flutter-apk/app-arm64-v8a-release.apk 56 | build/app/outputs/flutter-apk/app-arm64-v8a-release.apk.sha1 57 | build/app/outputs/flutter-apk/app-x86_64-release.apk 58 | build/app/outputs/flutter-apk/app-x86_64-release.apk.sha1 59 | body: ${{ steps.changelog.outputs.latest }} 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.log 3 | *.pyc 4 | *.swp 5 | *.class 6 | .history 7 | .DS_Store 8 | .svn/ 9 | .atom/ 10 | .buildlog/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | 20 | # Flutter/Dart/Pub related 21 | **/doc/api/ 22 | **/ios/Flutter/.last_build_id 23 | .flutter-plugins 24 | devtools_options.yaml 25 | .flutter-plugins-dependencies 26 | .pub/ 27 | /build/ 28 | .pub-cache/ 29 | .dart_tool/ 30 | 31 | # Obfuscation related 32 | app.*.map.json 33 | 34 | # Symbolication related 35 | app.*.symbols 36 | 37 | # Android Studio related 38 | /android/app/debug 39 | /android/app/profile 40 | /android/app/release 41 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 17 | base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 18 | - platform: linux 19 | create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 20 | base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 21 | - platform: windows 22 | create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 23 | base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 24 | 25 | # User provided section 26 | 27 | # List of Local paths (relative to this file) that should be 28 | # ignored by the migrate tool. 29 | # 30 | # Files that are not part of the templates will be ignored by default. 31 | unmanaged_files: 32 | - 'lib/main.dart' 33 | - 'ios/Runner.xcodeproj/project.pbxproj' 34 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 0.3.5 (2025-01-10) 2 | 3 | * Add: General Web Search. 4 | * Improve: Additional page details. 5 | 6 | * 增加:通用联网搜索 7 | * 改进:更多页面细节 8 | 9 | 0.3.4 (2025-01-02) 10 | 11 | * Improve: Chat Settings. 12 | * Improve: Model Settings. 13 | * Add: Data Clearing. 14 | 15 | * 改进:对话设置页面 16 | * 改进:模型设置功能 17 | * 增加:清除数据功能 18 | 19 | 0.3.3 (2024-12-27) 20 | 21 | * Improve: Chat Settings Page. 22 | * Improve: Input Widget. 23 | 24 | * 改进:对话设置页面 25 | * 改进:输入组件 26 | 27 | 0.3.2 (2024-12-25) 28 | 29 | * Add: Title Generation. 30 | * Improve: Input Widget. 31 | * Fix: Google Gemini API proxy not working. 32 | 33 | * 增加:标题生成 34 | * 改进:输入组件 35 | * 修复:Google Gemini 接口代理无效。 36 | 37 | 0.3.1 (2024-12-17) 38 | 39 | * Add: Workspace. 40 | * Improve: Input Widget. 41 | 42 | * 增加:工作空间 43 | * 改进:输入组件 44 | 45 | 0.2.10 (2024-12-13) 46 | 47 | * Add: Checking for updates. 48 | * Improve: Images display. 49 | 50 | * 增加:检查更新 51 | * 改进:图片显示 52 | 53 | 0.2.9 (2024-12-13) 54 | 55 | * Add: Google Search support (gemini-2.0-flash-exp only) 56 | * Add: Support for sending multiple images. 57 | 58 | * 增加:支持 GoogleSearch(仅 gemini-2.0-flash-exp) 59 | * 增加:支持发送多张图片 60 | 61 | 0.2.8 (2024-12-09) 62 | 63 | * Add: Support Google Gemini API. 64 | * Improve: Chat Response. 65 | 66 | * 增加:支持 Google Gemini 接口 67 | * 改进:对话响应 68 | 69 | 0.2.7 (2024-12-06) 70 | 71 | * Add: Export chat as image. 72 | * Improve: Refine UI details. 73 | 74 | * 增加:将对话导出为图片 75 | * 改进:调整了界面细节 76 | 77 | 0.2.6 (2024-12-04) 78 | 79 | * Improve: LaTeX Rendering. 80 | 81 | * 改进:LaTeX 渲染 82 | 83 | 0.2.5 (2024-12-04) 84 | 85 | * Add: Image Generation. 86 | * Improve: Message List. 87 | * Improve: Configuration Loading and Export. 88 | 89 | * 增加:图像生成 90 | * 改进:消息列表 91 | * 改进:配置加载及导出 92 | 93 | 0.2.4 (2024-11-29) 94 | 95 | * Add: Image compression controls. 96 | * Improve: Redesigned settings page. 97 | 98 | * 增加:图片压缩控制 99 | * 改进:重新设计的设置页 100 | 101 | 0.2.3 (2024-11-27) 102 | 103 | * Add: A new app icon. 104 | * Improve: Remove unnecessary animations. 105 | 106 | * 增加:新的应用图标 107 | * 改进:删除不必要的动画 108 | 109 | 0.2.2 (2024-11-25) 110 | 111 | * Add: Chat clone and clear functionality. 112 | * Improve: More animations. 113 | 114 | * 增加:对话复制和清空 115 | * 改进:更多动画 116 | 117 | 0.2.1 (2024-11-23) 118 | 119 | * Improve: The Reanswering can be stopped. 120 | 121 | * 改进:可以终止重新回答 122 | 123 | 0.2.0 (2024-11-23) 124 | 125 | * Add: Reanswer. 126 | * Improve: TTS button. 127 | 128 | * 增加:重新回答 129 | * 改进:文本转语音按钮 130 | 131 | 0.1.11 (2024-11-22) 132 | 133 | * Add: More informative error messages. 134 | * Improve: Style of the Message Widget. 135 | 136 | * 增加:更多错误提示 137 | * 改进:消息卡片组件样式 138 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | This project is no longer maintained. 2 | 3 | ChatBot is licensed under the GNU General Public License v3.0 - see the COPYING file for details. 4 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | linter: 4 | rules: 5 | 6 | analyzer: 7 | plugins: 8 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/to/reference-keystore 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "kotlin-android" 3 | id "com.android.application" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | def keystoreProperties = new Properties() 8 | def keystorePropertiesFile = rootProject.file("key.properties") 9 | if (keystorePropertiesFile.exists()) { 10 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 11 | } 12 | 13 | android { 14 | namespace = "cc.arthur63.chatbot" 15 | compileSdk = flutter.compileSdkVersion 16 | 17 | compileOptions { 18 | sourceCompatibility = JavaVersion.VERSION_17 19 | targetCompatibility = JavaVersion.VERSION_17 20 | } 21 | 22 | kotlinOptions { 23 | jvmTarget = JavaVersion.VERSION_17 24 | } 25 | 26 | defaultConfig { 27 | minSdk = flutter.minSdkVersion 28 | versionCode = flutter.versionCode 29 | versionName = flutter.versionName 30 | targetSdk = flutter.targetSdkVersion 31 | applicationId = "cc.arthur63.chatbot" 32 | } 33 | 34 | signingConfigs { 35 | release { 36 | keyAlias keystoreProperties["keyAlias"] 37 | keyPassword keystoreProperties["keyPassword"] 38 | storePassword keystoreProperties["storePassword"] 39 | storeFile keystoreProperties["storeFile"] ? file(keystoreProperties["storeFile"]) : null 40 | } 41 | } 42 | 43 | buildTypes { 44 | release { 45 | minifyEnabled true 46 | shrinkResources true 47 | signingConfig signingConfigs.release 48 | } 49 | } 50 | } 51 | 52 | flutter { 53 | source = "../.." 54 | } 55 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/cc/arthur63/chatbot/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package cc.arthur63.chatbot 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fanenr/flutter-chatbot/b2508200fc4cc68d20249044d28b2e3fb73e241a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = "../build" 9 | 10 | subprojects { 11 | project.buildDir = "${rootProject.buildDir}/${project.name}" 12 | } 13 | 14 | subprojects { 15 | project.evaluationDependsOn(":app") 16 | } 17 | 18 | tasks.register("clean", Delete) { 19 | delete rootProject.buildDir 20 | } 21 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | android.useAndroidX=true 2 | android.enableJetifier=true 3 | org.gradle.daemon=false 4 | org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G 5 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | zipStorePath=wrapper/dists 2 | zipStoreBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionBase=GRADLE_USER_HOME 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip 6 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version "8.3.2" apply false 22 | id "org.jetbrains.kotlin.android" version "1.8.22" apply false 23 | } 24 | 25 | include ":app" 26 | -------------------------------------------------------------------------------- /assets/images/claude.svg: -------------------------------------------------------------------------------- 1 | Claude -------------------------------------------------------------------------------- /assets/images/deepseek.svg: -------------------------------------------------------------------------------- 1 | DeepSeek -------------------------------------------------------------------------------- /assets/images/gemini.svg: -------------------------------------------------------------------------------- 1 | Gemini -------------------------------------------------------------------------------- /assets/images/grok.svg: -------------------------------------------------------------------------------- 1 | Grok -------------------------------------------------------------------------------- /assets/images/ollama.svg: -------------------------------------------------------------------------------- 1 | Ollama -------------------------------------------------------------------------------- /assets/images/openai.svg: -------------------------------------------------------------------------------- 1 | OpenAI -------------------------------------------------------------------------------- /assets/images/qwen.svg: -------------------------------------------------------------------------------- 1 | Qwen -------------------------------------------------------------------------------- /lib/chat/current.dart: -------------------------------------------------------------------------------- 1 | // This file is part of ChatBot. 2 | // 3 | // ChatBot is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // ChatBot is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with ChatBot. If not, see . 15 | 16 | import "message.dart"; 17 | import "../util.dart"; 18 | import "../config.dart"; 19 | 20 | import "dart:io"; 21 | import "dart:isolate"; 22 | import "dart:convert"; 23 | 24 | class Current { 25 | static File? _file; 26 | static ChatConfig? _chat; 27 | 28 | static CoreConfig core = Config.core; 29 | static final List messages = []; 30 | static TtsStatus ttsStatus = TtsStatus.nothing; 31 | static ChatStatus chatStatus = ChatStatus.nothing; 32 | 33 | static Future load(ChatConfig chat) async { 34 | _file = File(Config.chatFilePath(chat.fileName)); 35 | final from = _file; 36 | 37 | final json = await Isolate.run(() async { 38 | return jsonDecode(await from!.readAsString()); 39 | }); 40 | 41 | final messagesJson = json["messages"] ?? []; 42 | final coreJson = json["core"]; 43 | 44 | messages.clear(); 45 | for (final message in messagesJson) { 46 | messages.add(Message.fromJson(message)); 47 | } 48 | 49 | core = coreJson != null ? CoreConfig.fromJson(coreJson) : Config.core; 50 | } 51 | 52 | static Future save() async { 53 | await _file!.writeAsString(jsonEncode({ 54 | "core": core, 55 | "messages": messages, 56 | })); 57 | } 58 | 59 | static void clear() { 60 | _chat = null; 61 | _file = null; 62 | messages.clear(); 63 | core = Config.core; 64 | } 65 | 66 | static void newChat(String title) { 67 | final now = DateTime.now(); 68 | final timestamp = now.millisecondsSinceEpoch.toString(); 69 | 70 | final time = Util.formatDateTime(now); 71 | final fileName = "$timestamp.json"; 72 | 73 | _chat = ChatConfig( 74 | time: time, 75 | title: title, 76 | fileName: fileName, 77 | ); 78 | _file = File(Config.chatFilePath(fileName)); 79 | 80 | Config.chats.insert(0, _chat!); 81 | Config.save(); 82 | } 83 | 84 | static String? get bot => core.bot; 85 | static String? get api => core.api; 86 | static String? get model => core.model; 87 | 88 | static ApiConfig? get _api => Config.apis[api]; 89 | static BotConfig? get _bot => Config.bots[bot]; 90 | 91 | static String? get apiUrl => _api?.url; 92 | static String? get apiKey => _api?.key; 93 | static String? get apiType => _api?.type; 94 | 95 | static bool? get stream => _bot?.stream; 96 | static int? get maxTokens => _bot?.maxTokens; 97 | static double? get temperature => _bot?.temperature; 98 | static String? get systemPrompts => _bot?.systemPrompts; 99 | 100 | static ChatConfig? get chat => _chat; 101 | static set chat(ChatConfig? chat) => _chat = chat!; 102 | 103 | static String? get title => _chat?.title; 104 | static set title(String? title) => _chat?.title = title!; 105 | 106 | static bool get hasChat => _chat != null; 107 | } 108 | 109 | enum TtsStatus { 110 | nothing, 111 | loading, 112 | playing; 113 | 114 | bool get isNothing => this == TtsStatus.nothing; 115 | bool get isLoading => this == TtsStatus.loading; 116 | bool get isPlaying => this == TtsStatus.playing; 117 | } 118 | 119 | enum ChatStatus { 120 | nothing, 121 | responding; 122 | 123 | bool get isNothing => this == ChatStatus.nothing; 124 | bool get isResponding => this == ChatStatus.responding; 125 | } 126 | -------------------------------------------------------------------------------- /lib/chat/input.dart: -------------------------------------------------------------------------------- 1 | // This file is part of ChatBot. 2 | // 3 | // ChatBot is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // ChatBot is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with ChatBot. If not, see . 15 | 16 | import "chat.dart"; 17 | import "message.dart"; 18 | import "current.dart"; 19 | import "../util.dart"; 20 | import "../config.dart"; 21 | import "../llm/llm.dart"; 22 | import "../gen/l10n.dart"; 23 | 24 | import "dart:convert"; 25 | import "package:flutter/material.dart"; 26 | import "package:flutter/services.dart"; 27 | import "package:image_picker/image_picker.dart"; 28 | import "package:flutter_riverpod/flutter_riverpod.dart"; 29 | import "package:flutter_image_compress/flutter_image_compress.dart"; 30 | 31 | class InputWidget extends ConsumerStatefulWidget { 32 | static final FocusNode focusNode = FocusNode(); 33 | 34 | const InputWidget({super.key}); 35 | 36 | @override 37 | ConsumerState createState() => _InputWidgetState(); 38 | 39 | static void unFocus() => focusNode.unfocus(); 40 | } 41 | 42 | typedef _Image = ({String name, MessageImage image}); 43 | 44 | class _InputWidgetState extends ConsumerState { 45 | String _lastText = ""; 46 | static final List<_Image> _images = []; 47 | final TextEditingController _inputCtrl = TextEditingController(); 48 | 49 | @override 50 | void initState() { 51 | super.initState(); 52 | _inputCtrl.addListener(() { 53 | final text = _inputCtrl.text; 54 | if (_lastText.isEmpty ^ text.isEmpty) { 55 | setState(() {}); 56 | } 57 | _lastText = text; 58 | }); 59 | } 60 | 61 | @override 62 | void dispose() { 63 | _inputCtrl.dispose(); 64 | super.dispose(); 65 | } 66 | 67 | @override 68 | Widget build(BuildContext context) { 69 | ref.watch(llmProvider); 70 | 71 | return Container( 72 | constraints: BoxConstraints( 73 | maxHeight: MediaQuery.of(context).size.height / 4, 74 | ), 75 | decoration: BoxDecoration( 76 | color: Theme.of(context).colorScheme.surfaceContainerHigh, 77 | borderRadius: const BorderRadius.all(Radius.circular(12)), 78 | border: Border.all( 79 | color: Theme.of(context).colorScheme.outlineVariant, 80 | ), 81 | ), 82 | child: Column( 83 | mainAxisSize: MainAxisSize.min, 84 | children: [ 85 | Flexible( 86 | child: TextField( 87 | maxLines: null, 88 | autofocus: false, 89 | controller: _inputCtrl, 90 | focusNode: InputWidget.focusNode, 91 | keyboardType: TextInputType.multiline, 92 | decoration: InputDecoration( 93 | border: InputBorder.none, 94 | constraints: const BoxConstraints(), 95 | hintText: S.of(context).enter_message, 96 | contentPadding: const EdgeInsets.only( 97 | top: 8, left: 16, right: 16, bottom: 8), 98 | ), 99 | ), 100 | ), 101 | Row( 102 | children: [ 103 | const SizedBox(width: 8), 104 | SizedBox.square( 105 | dimension: 36, 106 | child: IconButton( 107 | icon: const Icon(Icons.upload_file), 108 | padding: EdgeInsets.zero, 109 | onPressed: _addFile, 110 | ), 111 | ), 112 | const SizedBox(width: 8), 113 | SizedBox.square( 114 | dimension: 36, 115 | child: GestureDetector( 116 | onLongPress: () { 117 | final old = Preferences.googleSearch; 118 | Util.showSnackBar( 119 | context: context, 120 | content: Text(old 121 | ? S.of(context).search_general_mode 122 | : S.of(context).search_gemini_mode), 123 | ); 124 | setState(() => Preferences.googleSearch = !old); 125 | }, 126 | child: IconButton( 127 | icon: const Icon(Icons.language), 128 | isSelected: Preferences.search, 129 | selectedIcon: Badge( 130 | label: const Text("G"), 131 | isLabelVisible: Preferences.googleSearch, 132 | child: const Icon(Icons.language), 133 | ), 134 | padding: EdgeInsets.zero, 135 | onPressed: () => setState( 136 | () => Preferences.search = !Preferences.search), 137 | ), 138 | ), 139 | ), 140 | if (_images.isNotEmpty) ...[ 141 | const SizedBox(width: 8), 142 | SizedBox.square( 143 | dimension: 36, 144 | child: IconButton( 145 | icon: const Icon(Icons.image_outlined), 146 | isSelected: true, 147 | padding: EdgeInsets.zero, 148 | onPressed: _editImages, 149 | ), 150 | ), 151 | ], 152 | const Expanded(child: SizedBox()), 153 | const SizedBox(width: 8), 154 | SizedBox.square( 155 | dimension: 36, 156 | child: _buildSendButton(), 157 | ), 158 | const SizedBox(width: 8), 159 | ], 160 | ), 161 | const SizedBox(height: 8), 162 | ], 163 | ), 164 | ); 165 | } 166 | 167 | Widget _buildSendButton() { 168 | IconData icon; 169 | Color background; 170 | Color? foreground; 171 | 172 | if (Current.chatStatus.isResponding) { 173 | icon = Icons.pause; 174 | background = Theme.of(context).colorScheme.primaryContainer; 175 | foreground = Theme.of(context).colorScheme.onPrimaryContainer; 176 | } else { 177 | icon = Icons.arrow_upward; 178 | if (_inputCtrl.text.isEmpty) { 179 | background = Colors.grey.withOpacity(0.2); 180 | } else { 181 | background = Theme.of(context).colorScheme.primaryContainer; 182 | foreground = Theme.of(context).colorScheme.onPrimaryContainer; 183 | } 184 | } 185 | 186 | return IconButton( 187 | icon: Icon(icon), 188 | onPressed: _sendMessage, 189 | padding: EdgeInsets.zero, 190 | style: IconButton.styleFrom( 191 | foregroundColor: foreground, 192 | backgroundColor: background, 193 | ), 194 | ); 195 | } 196 | 197 | Future _addFile() async { 198 | InputWidget.unFocus(); 199 | 200 | final result = await showModalBottomSheet( 201 | context: context, 202 | builder: (context) => Padding( 203 | padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), 204 | child: Column( 205 | mainAxisSize: MainAxisSize.min, 206 | children: [ 207 | const DragHandle(), 208 | ListTile( 209 | minTileHeight: 48, 210 | shape: const StadiumBorder(), 211 | title: Text(S.of(context).camera), 212 | leading: const Icon(Icons.camera_outlined), 213 | onTap: () => Navigator.of(context).pop(1), 214 | ), 215 | ListTile( 216 | minTileHeight: 48, 217 | shape: const StadiumBorder(), 218 | title: Text(S.of(context).gallery), 219 | leading: const Icon(Icons.photo_library_outlined), 220 | onTap: () => Navigator.of(context).pop(2), 221 | ), 222 | ], 223 | ), 224 | ), 225 | ); 226 | 227 | switch (result) { 228 | case 1: 229 | await _addImage(ImageSource.camera); 230 | break; 231 | 232 | case 2: 233 | await _addImage(ImageSource.gallery); 234 | break; 235 | } 236 | } 237 | 238 | Future _addImage(ImageSource source) async { 239 | XFile? result; 240 | Uint8List? compressed; 241 | final picker = ImagePicker(); 242 | 243 | try { 244 | result = await picker.pickImage(source: source); 245 | if (result == null) return; 246 | } catch (e) { 247 | return; 248 | } 249 | 250 | if (Config.cic.enable ?? true) { 251 | try { 252 | compressed = await FlutterImageCompress.compressWithFile( 253 | result.path, 254 | quality: Config.cic.quality ?? 95, 255 | minWidth: Config.cic.minWidth ?? 1920, 256 | minHeight: Config.cic.minHeight ?? 1080, 257 | ); 258 | if (compressed == null) throw false; 259 | } catch (e) { 260 | if (mounted) { 261 | Util.showSnackBar( 262 | context: context, 263 | content: Text(S.of(context).image_compress_failed), 264 | ); 265 | } 266 | } 267 | } 268 | 269 | final bytes = compressed ?? await result.readAsBytes(); 270 | final image = ( 271 | name: result.name, 272 | image: (bytes: bytes, base64: base64Encode(bytes)), 273 | ); 274 | setState(() => _images.add(image)); 275 | } 276 | 277 | void _editImages() async { 278 | InputWidget.unFocus(); 279 | 280 | showModalBottomSheet( 281 | context: context, 282 | scrollControlDisabledMaxHeightRatio: 0.7, 283 | builder: (context) => StatefulBuilder( 284 | builder: (context, setState2) => Column( 285 | mainAxisSize: MainAxisSize.min, 286 | crossAxisAlignment: CrossAxisAlignment.start, 287 | children: [ 288 | DialogHeader(title: S.of(context).images), 289 | const Divider(height: 1), 290 | ListView.builder( 291 | shrinkWrap: true, 292 | itemCount: _images.length, 293 | itemBuilder: (context, index) => ListTile( 294 | title: Text(_images[index].name), 295 | contentPadding: const EdgeInsets.only(left: 24, right: 12), 296 | trailing: IconButton( 297 | icon: const Icon(Icons.delete), 298 | onPressed: () { 299 | setState(() => _images.removeAt(index)); 300 | if (_images.isEmpty) { 301 | Navigator.of(context).pop(); 302 | } else { 303 | setState2(() {}); 304 | } 305 | }, 306 | ), 307 | ), 308 | ), 309 | const Divider(height: 1), 310 | DialogFooter( 311 | children: [ 312 | TextButton( 313 | onPressed: Navigator.of(context).pop, 314 | child: Text(S.of(context).cancel), 315 | ), 316 | TextButton( 317 | onPressed: () { 318 | setState(() => _images.clear()); 319 | Navigator.of(context).pop(); 320 | }, 321 | child: Text(S.of(context).clear), 322 | ), 323 | ], 324 | ), 325 | ], 326 | ), 327 | ), 328 | ); 329 | } 330 | 331 | Future _sendMessage() async { 332 | if (!Current.chatStatus.isNothing) { 333 | ref.read(llmProvider.notifier).stopChat(); 334 | return; 335 | } 336 | 337 | if (!Util.checkChat(context)) return; 338 | final text = _inputCtrl.text; 339 | if (text.isEmpty) return; 340 | 341 | final messages = Current.messages; 342 | final user = MessageItem( 343 | text: text, 344 | role: MessageRole.user, 345 | ); 346 | for (final image in _images) { 347 | user.images.add(image.image); 348 | } 349 | messages.add(Message.fromItem(user)); 350 | 351 | final assistant = Message.fromItem(MessageItem( 352 | text: "", 353 | model: Current.model, 354 | role: MessageRole.assistant, 355 | time: Util.formatDateTime(DateTime.now()), 356 | )); 357 | messages.add(assistant); 358 | ref.read(messagesProvider.notifier).notify(); 359 | 360 | _images.clear(); 361 | _inputCtrl.clear(); 362 | 363 | final results = await Future.wait([ 364 | _generateTitle(text).catchError((e) => text), 365 | ref.read(llmProvider.notifier).chat(assistant), 366 | ]); 367 | 368 | final title = results[0]; 369 | final error = results[1]; 370 | 371 | if (error != null && mounted) { 372 | _inputCtrl.text = text; 373 | Dialogs.error(context: context, error: error); 374 | } 375 | 376 | if (title != null) { 377 | Current.newChat(title); 378 | ref.read(chatProvider.notifier).notify(); 379 | ref.read(chatsProvider.notifier).notify(); 380 | } 381 | Current.save(); 382 | } 383 | 384 | Future _generateTitle(String text) async { 385 | if (Current.hasChat) return null; 386 | return await generateTitle(text); 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /lib/chat/settings.dart: -------------------------------------------------------------------------------- 1 | // This file is part of ChatBot. 2 | // 3 | // ChatBot is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // ChatBot is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with ChatBot. If not, see . 15 | 16 | import "chat.dart"; 17 | import "current.dart"; 18 | import "../util.dart"; 19 | import "../config.dart"; 20 | import "../gen/l10n.dart"; 21 | import "../settings/api.dart"; 22 | import "../settings/bot.dart"; 23 | import "../workspace/model.dart"; 24 | 25 | import "package:flutter/material.dart"; 26 | import "package:flutter_riverpod/flutter_riverpod.dart"; 27 | 28 | class ChatSettings extends ConsumerStatefulWidget { 29 | const ChatSettings({super.key}); 30 | 31 | @override 32 | ConsumerState createState() => _ChatSettingsState(); 33 | } 34 | 35 | class _ChatSettingsState extends ConsumerState { 36 | String? _error; 37 | String? _bot = Current.bot; 38 | String? _api = Current.api; 39 | String? _model = Current.model; 40 | final TextEditingController _ctrl = 41 | TextEditingController(text: Current.title); 42 | 43 | @override 44 | void dispose() { 45 | _ctrl.dispose(); 46 | super.dispose(); 47 | } 48 | 49 | @override 50 | Widget build(BuildContext context) { 51 | ref.watch(chatProvider); 52 | ref.watch(botsProvider); 53 | ref.watch(apisProvider); 54 | 55 | return Column( 56 | mainAxisSize: MainAxisSize.min, 57 | children: [ 58 | DialogHeader(title: S.of(context).chat_settings), 59 | const Divider(height: 1), 60 | const SizedBox(height: 4), 61 | Row( 62 | children: [ 63 | const SizedBox(width: 24), 64 | Expanded( 65 | child: TextField( 66 | controller: _ctrl, 67 | decoration: InputDecoration( 68 | errorText: _error, 69 | labelText: S.of(context).chat_title, 70 | border: const UnderlineInputBorder(), 71 | ), 72 | ), 73 | ), 74 | IconButton( 75 | icon: const Icon(Icons.subdirectory_arrow_left), 76 | onPressed: _saveTitle, 77 | ), 78 | const SizedBox(width: 12), 79 | ], 80 | ), 81 | const SizedBox(height: 12), 82 | Flexible( 83 | child: SingleChildScrollView( 84 | padding: const EdgeInsets.only(left: 12, right: 12), 85 | child: Column( 86 | mainAxisSize: MainAxisSize.min, 87 | children: [ 88 | _buildBots(), 89 | const SizedBox(height: 8), 90 | ConstrainedBox( 91 | constraints: const BoxConstraints(maxHeight: 320), 92 | child: IntrinsicHeight( 93 | child: Row( 94 | crossAxisAlignment: CrossAxisAlignment.start, 95 | children: [ 96 | Flexible( 97 | flex: 2, 98 | child: _buildApis(), 99 | ), 100 | const SizedBox(width: 8), 101 | Flexible( 102 | flex: 3, 103 | child: _buildModels(), 104 | ), 105 | ], 106 | ), 107 | ), 108 | ), 109 | const SizedBox(height: 12), 110 | ], 111 | ), 112 | ), 113 | ), 114 | ], 115 | ); 116 | } 117 | 118 | Widget _buildBots() { 119 | final bots = Config.bots.keys.toList(); 120 | 121 | return Card.filled( 122 | margin: EdgeInsets.zero, 123 | color: Theme.of(context).colorScheme.surfaceContainer, 124 | child: Column( 125 | crossAxisAlignment: CrossAxisAlignment.start, 126 | children: [ 127 | const SizedBox(height: 12), 128 | Row( 129 | children: [ 130 | const SizedBox(width: 12), 131 | Icon( 132 | Icons.smart_toy, 133 | color: Theme.of(context).colorScheme.primary, 134 | ), 135 | const SizedBox(width: 16), 136 | Text( 137 | S.of(context).bot, 138 | style: TextStyle( 139 | color: Theme.of(context).colorScheme.primary, 140 | ), 141 | ), 142 | ], 143 | ), 144 | const SizedBox(height: 12), 145 | if (bots.isNotEmpty) ...[ 146 | const Divider(height: 1), 147 | const SizedBox(height: 8), 148 | Stack( 149 | children: [ 150 | const IgnorePointer( 151 | child: Opacity( 152 | opacity: 0, 153 | child: ChoiceChip( 154 | label: Text("bot"), 155 | padding: EdgeInsets.all(4), 156 | selected: true, 157 | ), 158 | ), 159 | ), 160 | const SizedBox(width: double.infinity), 161 | Positioned.fill( 162 | child: ListView.separated( 163 | shrinkWrap: true, 164 | scrollDirection: Axis.horizontal, 165 | padding: const EdgeInsets.only(left: 12, right: 12), 166 | itemCount: bots.length, 167 | itemBuilder: (context, index) { 168 | final bot = bots[index]; 169 | return GestureDetector( 170 | onLongPress: () => 171 | Navigator.of(context).push(MaterialPageRoute( 172 | builder: (context) => BotSettings(bot: bot), 173 | )), 174 | child: ChoiceChip( 175 | label: Text(bot), 176 | padding: const EdgeInsets.all(4), 177 | selected: _bot == bot, 178 | onSelected: (value) { 179 | setState(() => _bot = value ? bot : null); 180 | _saveCore(); 181 | }, 182 | ), 183 | ); 184 | }, 185 | separatorBuilder: (context, index) => 186 | const SizedBox(width: 8), 187 | ), 188 | ), 189 | ], 190 | ), 191 | const SizedBox(height: 8), 192 | ], 193 | ], 194 | ), 195 | ); 196 | } 197 | 198 | Widget _buildApis() { 199 | final apis = Config.apis.keys.toList(); 200 | 201 | return Card.filled( 202 | margin: EdgeInsets.zero, 203 | color: Theme.of(context).colorScheme.surfaceContainer, 204 | child: Column( 205 | crossAxisAlignment: CrossAxisAlignment.start, 206 | children: [ 207 | const SizedBox(height: 12), 208 | Row( 209 | children: [ 210 | const SizedBox(width: 12), 211 | Icon( 212 | Icons.api, 213 | color: Theme.of(context).colorScheme.primary, 214 | ), 215 | const SizedBox(width: 16), 216 | Text( 217 | S.of(context).api, 218 | style: TextStyle( 219 | color: Theme.of(context).colorScheme.primary, 220 | ), 221 | ), 222 | ], 223 | ), 224 | const SizedBox(height: 12), 225 | if (apis.isNotEmpty) ...[ 226 | const Divider(height: 1), 227 | Flexible( 228 | child: SingleChildScrollView( 229 | child: Column( 230 | children: [ 231 | for (final api in apis) 232 | ListTile( 233 | title: Text(api), 234 | minTileHeight: 48, 235 | selected: _api == api, 236 | contentPadding: 237 | const EdgeInsets.only(left: 16, right: 16), 238 | onTap: () => setState(() => _api = api), 239 | onLongPress: () => 240 | Navigator.of(context).push(MaterialPageRoute( 241 | builder: (context) => ApiSettings(api: api), 242 | )), 243 | ), 244 | ], 245 | ), 246 | ), 247 | ), 248 | const SizedBox(height: 12), 249 | ], 250 | ], 251 | ), 252 | ); 253 | } 254 | 255 | Widget _buildModels() { 256 | final ids = Config.apis[_api]?.models ?? []; 257 | final models = <({String id, String name})>[]; 258 | 259 | for (final it in ids) { 260 | final config = Config.models[it]; 261 | if (config == null) { 262 | models.add((id: it, name: it)); 263 | } else if (config.chat) { 264 | models.add((id: it, name: config.name)); 265 | } 266 | } 267 | 268 | return Card.filled( 269 | margin: EdgeInsets.zero, 270 | color: Theme.of(context).colorScheme.surfaceContainer, 271 | child: Column( 272 | crossAxisAlignment: CrossAxisAlignment.start, 273 | children: [ 274 | const SizedBox(height: 12), 275 | Row( 276 | children: [ 277 | const SizedBox(width: 12), 278 | Icon( 279 | Icons.face, 280 | color: Theme.of(context).colorScheme.primary, 281 | ), 282 | const SizedBox(width: 16), 283 | Text( 284 | S.of(context).model, 285 | style: TextStyle( 286 | color: Theme.of(context).colorScheme.primary, 287 | ), 288 | ), 289 | ], 290 | ), 291 | const SizedBox(height: 12), 292 | if (ids.isNotEmpty) ...[ 293 | const Divider(height: 1), 294 | Flexible( 295 | child: SingleChildScrollView( 296 | child: Column( 297 | children: [ 298 | for (final model in models) 299 | ListTile( 300 | title: Text(model.name), 301 | minTileHeight: 48, 302 | selected: _model == model.id, 303 | contentPadding: 304 | const EdgeInsets.only(left: 16, right: 16), 305 | onTap: () { 306 | setState(() => _model = model.id); 307 | _saveCore(); 308 | }, 309 | onLongPress: () => showModalBottomSheet( 310 | context: context, 311 | useSafeArea: true, 312 | isScrollControlled: true, 313 | builder: (context) => Padding( 314 | padding: EdgeInsets.only( 315 | bottom: MediaQuery.of(context).viewInsets.bottom, 316 | ), 317 | child: ModelSettings(id: model.id), 318 | ), 319 | ), 320 | ), 321 | ], 322 | ), 323 | ), 324 | ), 325 | const SizedBox(height: 12), 326 | ], 327 | ], 328 | ), 329 | ); 330 | } 331 | 332 | void _saveCore() { 333 | final oldModel = Current.model; 334 | 335 | Current.core = CoreConfig( 336 | bot: _bot, 337 | api: _api, 338 | model: _model, 339 | ); 340 | 341 | if (_model != oldModel) { 342 | ref.read(chatProvider.notifier).notify(); 343 | } 344 | 345 | if (Current.hasChat) Current.save(); 346 | } 347 | 348 | void _saveTitle() { 349 | final title = _ctrl.text; 350 | final hasChat = Current.hasChat; 351 | final oldTitle = Current.title ?? ""; 352 | 353 | if (title.isEmpty && hasChat) { 354 | final error = S.of(context).enter_a_title; 355 | setState(() => _error = error); 356 | return; 357 | } 358 | 359 | if (title != oldTitle) { 360 | if (hasChat) { 361 | Current.title = title; 362 | } else { 363 | Current.newChat(title); 364 | } 365 | ref.read(chatProvider.notifier).notify(); 366 | ref.read(chatsProvider.notifier).notify(); 367 | } 368 | 369 | setState(() => _error = null); 370 | if (Current.hasChat) Current.save(); 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /lib/gen/intl/messages_all.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart 2 | // This is a library that looks up messages for specific locales by 3 | // delegating to the appropriate library. 4 | 5 | // Ignore issues from commonly used lints in this file. 6 | // ignore_for_file:implementation_imports, file_names, unnecessary_new 7 | // ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering 8 | // ignore_for_file:argument_type_not_assignable, invalid_assignment 9 | // ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases 10 | // ignore_for_file:comment_references 11 | 12 | import 'dart:async'; 13 | 14 | import 'package:flutter/foundation.dart'; 15 | import 'package:intl/intl.dart'; 16 | import 'package:intl/message_lookup_by_library.dart'; 17 | import 'package:intl/src/intl_helpers.dart'; 18 | 19 | import 'messages_en.dart' as messages_en; 20 | import 'messages_zh_CN.dart' as messages_zh_cn; 21 | 22 | typedef Future LibraryLoader(); 23 | Map _deferredLibraries = { 24 | 'en': () => new SynchronousFuture(null), 25 | 'zh_CN': () => new SynchronousFuture(null), 26 | }; 27 | 28 | MessageLookupByLibrary? _findExact(String localeName) { 29 | switch (localeName) { 30 | case 'en': 31 | return messages_en.messages; 32 | case 'zh_CN': 33 | return messages_zh_cn.messages; 34 | default: 35 | return null; 36 | } 37 | } 38 | 39 | /// User programs should call this before using [localeName] for messages. 40 | Future initializeMessages(String localeName) { 41 | var availableLocale = Intl.verifiedLocale( 42 | localeName, (locale) => _deferredLibraries[locale] != null, 43 | onFailure: (_) => null); 44 | if (availableLocale == null) { 45 | return new SynchronousFuture(false); 46 | } 47 | var lib = _deferredLibraries[availableLocale]; 48 | lib == null ? new SynchronousFuture(false) : lib(); 49 | initializeInternalMessageLookup(() => new CompositeMessageLookup()); 50 | messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); 51 | return new SynchronousFuture(true); 52 | } 53 | 54 | bool _messagesExistFor(String locale) { 55 | try { 56 | return _findExact(locale) != null; 57 | } catch (e) { 58 | return false; 59 | } 60 | } 61 | 62 | MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { 63 | var actualLocale = 64 | Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null); 65 | if (actualLocale == null) return null; 66 | return _findExact(actualLocale); 67 | } 68 | -------------------------------------------------------------------------------- /lib/gen/intl/messages_zh_CN.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart 2 | // This is a library that provides messages for a zh_CN locale. All the 3 | // messages from the main program should be duplicated here with the same 4 | // function name. 5 | 6 | // Ignore issues from commonly used lints in this file. 7 | // ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new 8 | // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering 9 | // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases 10 | // ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes 11 | // ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes 12 | 13 | import 'package:intl/intl.dart'; 14 | import 'package:intl/message_lookup_by_library.dart'; 15 | 16 | final messages = new MessageLookup(); 17 | 18 | typedef String MessageIfAbsent(String messageStr, List args); 19 | 20 | class MessageLookup extends MessageLookupByLibrary { 21 | String get localeName => 'zh_CN'; 22 | 23 | static String m0(pages, text) => 24 | "提示词模板中的 ${pages} 占位变量会被网页内容替换,${text} 占位变量会被用户消息替换。如不清楚,可留空以使用内置模板。"; 25 | 26 | static String m1(text) => "提示词模板中的 ${text} 占位变量会被用户消息替换。如不清楚,可留空以使用内置模板。"; 27 | 28 | final messages = _notInlinedMessages(_notInlinedMessages); 29 | static Map _notInlinedMessages(_) => { 30 | "all_chats": MessageLookupByLibrary.simpleMessage("所有对话"), 31 | "api": MessageLookupByLibrary.simpleMessage("接口"), 32 | "api_key": MessageLookupByLibrary.simpleMessage("接口密钥"), 33 | "api_type": MessageLookupByLibrary.simpleMessage("接口类型"), 34 | "api_url": MessageLookupByLibrary.simpleMessage("接口地址"), 35 | "apis": MessageLookupByLibrary.simpleMessage("接口"), 36 | "base_config": MessageLookupByLibrary.simpleMessage("基本配置"), 37 | "bot": MessageLookupByLibrary.simpleMessage("角色"), 38 | "bots": MessageLookupByLibrary.simpleMessage("角色"), 39 | "camera": MessageLookupByLibrary.simpleMessage("相机"), 40 | "cancel": MessageLookupByLibrary.simpleMessage("取消"), 41 | "cannot_open": MessageLookupByLibrary.simpleMessage("无法打开"), 42 | "chat_image_compress": MessageLookupByLibrary.simpleMessage("对话图片压缩"), 43 | "chat_model": MessageLookupByLibrary.simpleMessage("对话模型"), 44 | "chat_model_hint": MessageLookupByLibrary.simpleMessage("是否是对话模型?"), 45 | "chat_settings": MessageLookupByLibrary.simpleMessage("对话设置"), 46 | "chat_title": MessageLookupByLibrary.simpleMessage("对话标题"), 47 | "check_for_updates": MessageLookupByLibrary.simpleMessage("检查更新"), 48 | "choose_api": MessageLookupByLibrary.simpleMessage("选择接口"), 49 | "choose_bot": MessageLookupByLibrary.simpleMessage("选择角色"), 50 | "choose_model": MessageLookupByLibrary.simpleMessage("选择模型"), 51 | "chunk_n": MessageLookupByLibrary.simpleMessage("块数量"), 52 | "chunk_n_hint": MessageLookupByLibrary.simpleMessage("集成到上下文中的块数量"), 53 | "chunk_overlap": MessageLookupByLibrary.simpleMessage("块重叠"), 54 | "chunk_overlap_hint": 55 | MessageLookupByLibrary.simpleMessage("与上一块重叠部分的大小"), 56 | "chunk_size": MessageLookupByLibrary.simpleMessage("块大小"), 57 | "chunk_size_hint": MessageLookupByLibrary.simpleMessage("单块能包含的最大字符数"), 58 | "citations": MessageLookupByLibrary.simpleMessage("引用"), 59 | "clear": MessageLookupByLibrary.simpleMessage("清空"), 60 | "clear_chat": MessageLookupByLibrary.simpleMessage("清空对话"), 61 | "clear_data": MessageLookupByLibrary.simpleMessage("清理数据"), 62 | "clear_data_audio": MessageLookupByLibrary.simpleMessage("所有的 TTS 缓存"), 63 | "clear_data_chat": MessageLookupByLibrary.simpleMessage("所有的对话数据"), 64 | "clear_data_image": MessageLookupByLibrary.simpleMessage("所有的图片生成结果"), 65 | "cleared_successfully": MessageLookupByLibrary.simpleMessage("清理成功"), 66 | "clearing": MessageLookupByLibrary.simpleMessage("清理中..."), 67 | "clone_chat": MessageLookupByLibrary.simpleMessage("复制对话"), 68 | "cloned_successfully": MessageLookupByLibrary.simpleMessage("复制成功"), 69 | "complete_all_fields": MessageLookupByLibrary.simpleMessage("请填写所有字段"), 70 | "config": MessageLookupByLibrary.simpleMessage("配置"), 71 | "config_hint": MessageLookupByLibrary.simpleMessage( 72 | "为避免导出失败,建议将配置导出到 Documents 目录,或在 Download 下创建 ChatBot 子目录。"), 73 | "config_import_export": MessageLookupByLibrary.simpleMessage("配置导入导出"), 74 | "copied_successfully": MessageLookupByLibrary.simpleMessage("拷贝成功"), 75 | "copy": MessageLookupByLibrary.simpleMessage("拷贝"), 76 | "default_config": MessageLookupByLibrary.simpleMessage("默认配置"), 77 | "delete": MessageLookupByLibrary.simpleMessage("删除"), 78 | "delete_image": MessageLookupByLibrary.simpleMessage("删除图片"), 79 | "document": MessageLookupByLibrary.simpleMessage("文档"), 80 | "document_config": MessageLookupByLibrary.simpleMessage("文档配置"), 81 | "document_config_hint": MessageLookupByLibrary.simpleMessage( 82 | "文档会被划分为若干块,经过搜索比较后,最合适的几个块会被补充进上下文。"), 83 | "download": MessageLookupByLibrary.simpleMessage("下载"), 84 | "duplicate_api_name": MessageLookupByLibrary.simpleMessage("接口名重复"), 85 | "duplicate_bot_name": MessageLookupByLibrary.simpleMessage("角色名重复"), 86 | "edit": MessageLookupByLibrary.simpleMessage("编辑"), 87 | "embedding_vector": MessageLookupByLibrary.simpleMessage("嵌入向量"), 88 | "embedding_vector_info": MessageLookupByLibrary.simpleMessage( 89 | "批大小受限于接口服务商,建议查询后修改。向量维度为专业选项,非必要请勿填写。"), 90 | "empty": MessageLookupByLibrary.simpleMessage("空"), 91 | "empty_link": MessageLookupByLibrary.simpleMessage("空链接"), 92 | "enable": MessageLookupByLibrary.simpleMessage("启用"), 93 | "ensure_clear_chat": MessageLookupByLibrary.simpleMessage("确定要清空对话?"), 94 | "ensure_delete_image": MessageLookupByLibrary.simpleMessage("确定要删除图片?"), 95 | "enter_a_name": MessageLookupByLibrary.simpleMessage("请输入名称"), 96 | "enter_a_title": MessageLookupByLibrary.simpleMessage("请输入标题"), 97 | "enter_message": MessageLookupByLibrary.simpleMessage("输入你的消息"), 98 | "enter_prompts": MessageLookupByLibrary.simpleMessage("请输入提示词"), 99 | "error": MessageLookupByLibrary.simpleMessage("错误"), 100 | "export_chat_as_image": MessageLookupByLibrary.simpleMessage("导出图片"), 101 | "export_config": MessageLookupByLibrary.simpleMessage("导出配置"), 102 | "exported_successfully": MessageLookupByLibrary.simpleMessage("导出成功"), 103 | "exporting": MessageLookupByLibrary.simpleMessage("正在导出..."), 104 | "failed_to_export": 105 | MessageLookupByLibrary.simpleMessage("无法在该目录下写入文件。"), 106 | "gallery": MessageLookupByLibrary.simpleMessage("图库"), 107 | "generate": MessageLookupByLibrary.simpleMessage("生成"), 108 | "image_compress_failed": MessageLookupByLibrary.simpleMessage("图片压缩失败"), 109 | "image_enable_hint": MessageLookupByLibrary.simpleMessage("压缩失败则将使用原图"), 110 | "image_generation": MessageLookupByLibrary.simpleMessage("图像生成"), 111 | "image_hint": MessageLookupByLibrary.simpleMessage( 112 | "质量范围应在 1-100,质量越低压缩率越高。最小宽度与最小高度用于限制图片缩放,如不清楚,请留空。"), 113 | "image_quality": MessageLookupByLibrary.simpleMessage("图像质量"), 114 | "image_size": MessageLookupByLibrary.simpleMessage("图像尺寸"), 115 | "image_style": MessageLookupByLibrary.simpleMessage("图像风格"), 116 | "images": MessageLookupByLibrary.simpleMessage("图片"), 117 | "import_config": MessageLookupByLibrary.simpleMessage("导入配置"), 118 | "imported_successfully": MessageLookupByLibrary.simpleMessage("导入成功"), 119 | "importing": MessageLookupByLibrary.simpleMessage("正在导入..."), 120 | "invalid_max_tokens": MessageLookupByLibrary.simpleMessage("非法的最大输出"), 121 | "invalid_temperature": MessageLookupByLibrary.simpleMessage("非法的温度"), 122 | "link": MessageLookupByLibrary.simpleMessage("链接"), 123 | "max_tokens": MessageLookupByLibrary.simpleMessage("最大输出"), 124 | "min_height": MessageLookupByLibrary.simpleMessage("最小高度"), 125 | "min_width": MessageLookupByLibrary.simpleMessage("最小宽度"), 126 | "min_width_height": MessageLookupByLibrary.simpleMessage("最小宽高"), 127 | "model": MessageLookupByLibrary.simpleMessage("模型"), 128 | "model_avatar": MessageLookupByLibrary.simpleMessage("模型头像"), 129 | "model_list": MessageLookupByLibrary.simpleMessage("模型列表"), 130 | "model_name": MessageLookupByLibrary.simpleMessage("模型名称"), 131 | "name": MessageLookupByLibrary.simpleMessage("名称"), 132 | "new_api": MessageLookupByLibrary.simpleMessage("新接口"), 133 | "new_bot": MessageLookupByLibrary.simpleMessage("新角色"), 134 | "new_chat": MessageLookupByLibrary.simpleMessage("新对话"), 135 | "no_model": MessageLookupByLibrary.simpleMessage("无模型"), 136 | "not_implemented_yet": MessageLookupByLibrary.simpleMessage("还未实现"), 137 | "ok": MessageLookupByLibrary.simpleMessage("确定"), 138 | "open": MessageLookupByLibrary.simpleMessage("打开"), 139 | "optional_config": MessageLookupByLibrary.simpleMessage("可选配置"), 140 | "other": MessageLookupByLibrary.simpleMessage("其他"), 141 | "play": MessageLookupByLibrary.simpleMessage("播放"), 142 | "please_input": MessageLookupByLibrary.simpleMessage("请输入"), 143 | "quality": MessageLookupByLibrary.simpleMessage("质量"), 144 | "reanswer": MessageLookupByLibrary.simpleMessage("重答"), 145 | "reset": MessageLookupByLibrary.simpleMessage("重置"), 146 | "restart_app": MessageLookupByLibrary.simpleMessage("请重启应用以加载新配置。"), 147 | "save": MessageLookupByLibrary.simpleMessage("保存"), 148 | "saved_successfully": MessageLookupByLibrary.simpleMessage("保存成功"), 149 | "search_gemini_mode": 150 | MessageLookupByLibrary.simpleMessage("Google Search 模式"), 151 | "search_general_mode": MessageLookupByLibrary.simpleMessage("通用模式"), 152 | "search_n": MessageLookupByLibrary.simpleMessage("网页数量"), 153 | "search_n_hint": MessageLookupByLibrary.simpleMessage("检索的网页数量上限"), 154 | "search_prompt": MessageLookupByLibrary.simpleMessage("提示词"), 155 | "search_prompt_hint": 156 | MessageLookupByLibrary.simpleMessage("用于合成上下文的提示词模板"), 157 | "search_prompt_info": m0, 158 | "search_searxng": MessageLookupByLibrary.simpleMessage("SearXNG"), 159 | "search_searxng_base": MessageLookupByLibrary.simpleMessage("根地址"), 160 | "search_searxng_extra": MessageLookupByLibrary.simpleMessage("附加参数"), 161 | "search_searxng_extra_help": MessageLookupByLibrary.simpleMessage( 162 | "例如:engines=google&language=zh"), 163 | "search_searxng_hint": 164 | MessageLookupByLibrary.simpleMessage("SearXNG 实例"), 165 | "search_timeout": MessageLookupByLibrary.simpleMessage("超时时间"), 166 | "search_timeout_fetch": MessageLookupByLibrary.simpleMessage("抓取超时"), 167 | "search_timeout_fetch_help": 168 | MessageLookupByLibrary.simpleMessage("抓取网页内容的超时时间"), 169 | "search_timeout_hint": MessageLookupByLibrary.simpleMessage("检索的超时毫秒数"), 170 | "search_timeout_query": MessageLookupByLibrary.simpleMessage("检索超时"), 171 | "search_timeout_query_help": 172 | MessageLookupByLibrary.simpleMessage("请求 SearXNG 的超时时间"), 173 | "search_vector": MessageLookupByLibrary.simpleMessage("嵌入向量"), 174 | "search_vector_hint": MessageLookupByLibrary.simpleMessage("建议开启"), 175 | "select_models": MessageLookupByLibrary.simpleMessage("选择模型"), 176 | "settings": MessageLookupByLibrary.simpleMessage("设置"), 177 | "setup_api_model_first": 178 | MessageLookupByLibrary.simpleMessage("请先配置接口和模型"), 179 | "setup_searxng_first": 180 | MessageLookupByLibrary.simpleMessage("请先配置 SearXNG 实例"), 181 | "setup_tts_first": MessageLookupByLibrary.simpleMessage("请先配置文本转语音"), 182 | "setup_vector_first": 183 | MessageLookupByLibrary.simpleMessage("请先配置嵌入向量接口和模型"), 184 | "share": MessageLookupByLibrary.simpleMessage("分享"), 185 | "source": MessageLookupByLibrary.simpleMessage("源码"), 186 | "streaming_response": MessageLookupByLibrary.simpleMessage("流式响应"), 187 | "system_prompts": MessageLookupByLibrary.simpleMessage("系统提示词"), 188 | "task": MessageLookupByLibrary.simpleMessage("任务"), 189 | "temperature": MessageLookupByLibrary.simpleMessage("温度"), 190 | "text_to_speech": MessageLookupByLibrary.simpleMessage("文本转语音"), 191 | "title": MessageLookupByLibrary.simpleMessage("ChatBot"), 192 | "title_enable_hint": 193 | MessageLookupByLibrary.simpleMessage("禁用则将以用户消息为标题"), 194 | "title_generation": MessageLookupByLibrary.simpleMessage("标题生成"), 195 | "title_generation_hint": m1, 196 | "title_prompt": MessageLookupByLibrary.simpleMessage("提示词"), 197 | "title_prompt_hint": 198 | MessageLookupByLibrary.simpleMessage("用于生成标题的提示词模板"), 199 | "up_to_date": MessageLookupByLibrary.simpleMessage("已是最新版本"), 200 | "vector_batch_size": MessageLookupByLibrary.simpleMessage("批大小"), 201 | "vector_batch_size_hint": 202 | MessageLookupByLibrary.simpleMessage("单次请求能提交的最大块数量"), 203 | "vector_dimensions": MessageLookupByLibrary.simpleMessage("向量维度"), 204 | "vector_dimensions_hint": 205 | MessageLookupByLibrary.simpleMessage("嵌入向量模型输出向量的维度"), 206 | "voice": MessageLookupByLibrary.simpleMessage("音色"), 207 | "web_search": MessageLookupByLibrary.simpleMessage("联网搜索"), 208 | "workspace": MessageLookupByLibrary.simpleMessage("工作空间") 209 | }; 210 | } 211 | -------------------------------------------------------------------------------- /lib/image/config.dart: -------------------------------------------------------------------------------- 1 | // This file is part of ChatBot. 2 | // 3 | // ChatBot is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // ChatBot is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with ChatBot. If not, see . 15 | 16 | import "../util.dart"; 17 | import "../config.dart"; 18 | import "../gen/l10n.dart"; 19 | import "../settings/api.dart"; 20 | 21 | import "package:flutter/material.dart"; 22 | import "package:flutter_riverpod/flutter_riverpod.dart"; 23 | 24 | class ConfigTab extends ConsumerStatefulWidget { 25 | const ConfigTab({super.key}); 26 | 27 | @override 28 | ConsumerState createState() => _ConfigTabState(); 29 | } 30 | 31 | class _ConfigTabState extends ConsumerState { 32 | @override 33 | Widget build(BuildContext context) { 34 | ref.watch(apisProvider); 35 | 36 | final s = S.of(context); 37 | const padding = EdgeInsets.only(left: 24, right: 24); 38 | final primaryColor = Theme.of(context).colorScheme.primary; 39 | 40 | return ListView( 41 | padding: const EdgeInsets.only(top: 16, bottom: 8), 42 | children: [ 43 | Padding( 44 | padding: padding, 45 | child: Text( 46 | s.base_config, 47 | style: TextStyle(color: primaryColor), 48 | ), 49 | ), 50 | ListTile( 51 | title: Text(s.api), 52 | contentPadding: padding, 53 | subtitle: Text(Config.image.api ?? s.empty), 54 | onTap: () async { 55 | if (Config.apis.isEmpty) return; 56 | 57 | final api = await Dialogs.select( 58 | context: context, 59 | list: Config.apis.keys.toList(), 60 | selected: Config.image.api, 61 | title: s.choose_api, 62 | ); 63 | if (api == null) return; 64 | 65 | setState(() => Config.image.api = api); 66 | Config.save(); 67 | }, 68 | ), 69 | const Divider(height: 1), 70 | ListTile( 71 | title: Text(s.model), 72 | contentPadding: padding, 73 | subtitle: Text(Config.image.model ?? s.empty), 74 | onTap: () async { 75 | final models = Config.apis[Config.image.api]?.models; 76 | if (models == null) return; 77 | 78 | final model = await Dialogs.select( 79 | context: context, 80 | selected: Config.image.model, 81 | title: s.choose_model, 82 | list: models, 83 | ); 84 | if (model == null) return; 85 | 86 | setState(() => Config.image.model = model); 87 | Config.save(); 88 | }, 89 | ), 90 | Padding( 91 | padding: padding, 92 | child: Text( 93 | s.optional_config, 94 | style: TextStyle(color: primaryColor), 95 | ), 96 | ), 97 | ListTile( 98 | title: Text(s.image_size), 99 | contentPadding: padding, 100 | subtitle: Text(Config.image.size ?? s.empty), 101 | onTap: () async { 102 | final texts = await Dialogs.input( 103 | context: context, 104 | title: s.image_size, 105 | fields: [ 106 | InputDialogField( 107 | label: s.please_input, 108 | text: Config.image.size, 109 | ), 110 | ], 111 | ); 112 | if (texts == null) return; 113 | 114 | final text = texts[0].trim(); 115 | final size = text.isEmpty ? null : text; 116 | setState(() => Config.image.size = size); 117 | Config.save(); 118 | }, 119 | ), 120 | const Divider(height: 1), 121 | ListTile( 122 | title: Text(s.image_style), 123 | contentPadding: padding, 124 | subtitle: Text(Config.image.style ?? s.empty), 125 | onTap: () async { 126 | final texts = await Dialogs.input( 127 | context: context, 128 | title: s.image_style, 129 | fields: [ 130 | InputDialogField( 131 | label: s.please_input, 132 | text: Config.image.style, 133 | ), 134 | ], 135 | ); 136 | if (texts == null) return; 137 | 138 | final text = texts[0].trim(); 139 | final style = text.isEmpty ? null : text; 140 | setState(() => Config.image.style = style); 141 | Config.save(); 142 | }, 143 | ), 144 | const Divider(height: 1), 145 | ListTile( 146 | title: Text(s.image_quality), 147 | contentPadding: padding, 148 | subtitle: Text(Config.image.quality ?? s.empty), 149 | onTap: () async { 150 | final texts = await Dialogs.input( 151 | context: context, 152 | title: s.image_quality, 153 | fields: [ 154 | InputDialogField( 155 | label: s.please_input, 156 | text: Config.image.quality, 157 | ), 158 | ], 159 | ); 160 | if (texts == null) return; 161 | 162 | final text = texts[0].trim(); 163 | final quality = text.isEmpty ? null : text; 164 | setState(() => Config.image.quality = quality); 165 | Config.save(); 166 | }, 167 | ), 168 | ], 169 | ); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /lib/image/generate.dart: -------------------------------------------------------------------------------- 1 | // This file is part of ChatBot. 2 | // 3 | // ChatBot is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // ChatBot is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with ChatBot. If not, see . 15 | 16 | import "../util.dart"; 17 | import "../config.dart"; 18 | import "../gen/l10n.dart"; 19 | 20 | import "dart:io"; 21 | import "dart:convert"; 22 | import "package:http/http.dart"; 23 | import "package:flutter/material.dart"; 24 | import "package:flutter_riverpod/flutter_riverpod.dart"; 25 | 26 | enum _Status { 27 | nothing, 28 | loading, 29 | generating; 30 | 31 | bool get isNothing => this == _Status.nothing; 32 | bool get isLoading => this == _Status.loading; 33 | bool get isGenerating => this == _Status.generating; 34 | } 35 | 36 | class GenerateTab extends ConsumerStatefulWidget { 37 | const GenerateTab({super.key}); 38 | 39 | @override 40 | ConsumerState createState() => _GenerateTabState(); 41 | } 42 | 43 | class _GenerateTabState extends ConsumerState 44 | with AutomaticKeepAliveClientMixin { 45 | final TextEditingController _ctrl = TextEditingController(); 46 | final FocusNode _node = FocusNode(); 47 | _Status _status = _Status.nothing; 48 | final List _images = []; 49 | Client? _client; 50 | 51 | @override 52 | void dispose() { 53 | _node.dispose(); 54 | _ctrl.dispose(); 55 | super.dispose(); 56 | } 57 | 58 | @override 59 | bool get wantKeepAlive => true; 60 | 61 | @override 62 | Widget build(BuildContext context) { 63 | super.build(context); 64 | 65 | final decoration = BoxDecoration( 66 | color: Theme.of(context).colorScheme.surfaceContainerHighest, 67 | borderRadius: const BorderRadius.all(Radius.circular(12)), 68 | ); 69 | 70 | return ListView( 71 | padding: const EdgeInsets.all(16), 72 | children: [ 73 | Container( 74 | decoration: decoration, 75 | padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), 76 | child: TextField( 77 | maxLines: 4, 78 | focusNode: _node, 79 | controller: _ctrl, 80 | keyboardType: TextInputType.multiline, 81 | decoration: InputDecoration( 82 | hintText: S.of(context).enter_prompts, 83 | border: InputBorder.none, 84 | ), 85 | ), 86 | ), 87 | const SizedBox(height: 12), 88 | FilledButton.icon( 89 | icon: Icon(_status.isNothing ? Icons.add : Icons.close), 90 | label: Text( 91 | _status.isNothing ? S.of(context).generate : S.of(context).cancel, 92 | ), 93 | onPressed: _generate, 94 | ), 95 | const SizedBox(height: 12), 96 | if (!_status.isNothing) LinearProgressIndicator(), 97 | if (_images.isNotEmpty) 98 | AspectRatio( 99 | aspectRatio: _getAspectRatio(), 100 | child: Ink( 101 | decoration: BoxDecoration( 102 | borderRadius: const BorderRadius.all(Radius.circular(12)), 103 | image: DecorationImage( 104 | image: FileImage(File(_images.first)), 105 | fit: BoxFit.fitWidth, 106 | ), 107 | ), 108 | child: InkWell( 109 | borderRadius: const BorderRadius.all(Radius.circular(12)), 110 | onTap: () => Dialogs.handleImage( 111 | context: context, 112 | path: _images.first, 113 | ), 114 | ), 115 | ), 116 | ), 117 | ], 118 | ); 119 | } 120 | 121 | Future _generate() async { 122 | if (_status.isGenerating) { 123 | _status = _Status.nothing; 124 | _client?.close(); 125 | _client = null; 126 | return; 127 | } 128 | 129 | final prompt = _ctrl.text; 130 | if (prompt.isEmpty) return; 131 | 132 | final image = Config.image; 133 | final model = image.model; 134 | final api = Config.apis[image.api]; 135 | 136 | if (model == null || api == null) { 137 | Util.showSnackBar( 138 | context: context, 139 | content: Text(S.of(context).setup_api_model_first), 140 | ); 141 | return; 142 | } 143 | 144 | final apiUrl = api.url; 145 | final apiKey = api.key; 146 | final endPoint = "$apiUrl/images/generations"; 147 | 148 | setState(() { 149 | _images.clear(); 150 | _status = _Status.generating; 151 | }); 152 | 153 | final size = image.size; 154 | final style = image.style; 155 | final quality = image.quality; 156 | final optional = {}; 157 | 158 | if (size != null) optional["size"] = size; 159 | if (style != null) optional["style"] = style; 160 | if (quality != null) optional["quality"] = quality; 161 | 162 | try { 163 | _client ??= Client(); 164 | final genRes = await _client!.post( 165 | Uri.parse(endPoint), 166 | headers: { 167 | "Authorization": "Bearer $apiKey", 168 | "Content-Type": "application/json", 169 | }, 170 | body: jsonEncode({ 171 | ...optional, 172 | "model": model, 173 | "prompt": prompt, 174 | }), 175 | ); 176 | if (genRes.statusCode != 200) { 177 | throw "${genRes.statusCode} ${genRes.body}"; 178 | } 179 | 180 | _status = _Status.loading; 181 | 182 | final json = jsonDecode(genRes.body); 183 | final url = json["data"][0]["url"]; 184 | 185 | final loadRes = await _client!.get(Uri.parse(url)); 186 | if (loadRes.statusCode != 200) { 187 | throw "${genRes.statusCode} ${genRes.body}"; 188 | } 189 | 190 | final timestamp = DateTime.now().millisecondsSinceEpoch.toString(); 191 | final path = Config.imageFilePath("$timestamp.png"); 192 | 193 | final file = File(path); 194 | await file.writeAsBytes(loadRes.bodyBytes); 195 | 196 | if (!mounted) return; 197 | setState(() => _images.add(path)); 198 | } catch (e) { 199 | if (!mounted) return; 200 | if (_status.isGenerating) { 201 | Dialogs.error(context: context, error: e); 202 | } 203 | } 204 | 205 | if (!mounted) return; 206 | setState(() => _status = _Status.nothing); 207 | } 208 | 209 | double _getAspectRatio() { 210 | final size = Config.image.size; 211 | if (size == null) return 1; 212 | 213 | final pos = size.indexOf('x'); 214 | if (pos == -1) return 1; 215 | 216 | final width = size.substring(0, pos); 217 | final height = size.substring(pos + 1); 218 | 219 | final w = int.tryParse(width); 220 | final h = int.tryParse(height); 221 | if (w == null || h == null) return 1; 222 | 223 | return w / h; 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /lib/image/image.dart: -------------------------------------------------------------------------------- 1 | // This file is part of ChatBot. 2 | // 3 | // ChatBot is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // ChatBot is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with ChatBot. If not, see . 15 | 16 | import "config.dart"; 17 | import "generate.dart"; 18 | import "../gen/l10n.dart"; 19 | 20 | import "package:flutter/material.dart"; 21 | import "package:flutter_riverpod/flutter_riverpod.dart"; 22 | 23 | class ImagePage extends ConsumerWidget { 24 | const ImagePage({super.key}); 25 | 26 | @override 27 | Widget build(BuildContext context, WidgetRef ref) { 28 | return DefaultTabController( 29 | length: 2, 30 | child: Scaffold( 31 | appBar: AppBar( 32 | title: Text(S.of(context).image_generation), 33 | bottom: TabBar( 34 | tabs: [ 35 | Tab(text: S.of(context).generate), 36 | Tab(text: S.of(context).config), 37 | ], 38 | ), 39 | ), 40 | body: TabBarView( 41 | children: [ 42 | const GenerateTab(), 43 | const ConfigTab(), 44 | ], 45 | ), 46 | ), 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/l10n/intl_en.arb: -------------------------------------------------------------------------------- 1 | { 2 | "@@locale": "en", 3 | "title": "ChatBot", 4 | 5 | "ok": "Ok", 6 | "copy": "Copy", 7 | "edit": "Edit", 8 | "play": "Play", 9 | "source": "Source", 10 | "delete": "Delete", 11 | "images": "Images", 12 | "camera": "Camera", 13 | "gallery": "Gallery", 14 | "settings": "Settings", 15 | 16 | "bot": "Bot", 17 | "bots": "Bots", 18 | "apis": "APIs", 19 | "config": "Config", 20 | 21 | "open": "Open", 22 | "save": "Save", 23 | "reset": "Reset", 24 | "error": "Error", 25 | "share": "Share", 26 | "clear": "Clear", 27 | "cancel": "Cancel", 28 | "reanswer": "Reanswer", 29 | "citations": "Citations", 30 | 31 | "api": "API", 32 | "model": "Model", 33 | "voice": "Voice", 34 | "max_tokens": "Max Tokens", 35 | "temperature": "Temperature", 36 | "system_prompts": "System Prompts", 37 | "streaming_response": "Streaming Response", 38 | 39 | "link": "Link", 40 | "name": "Name", 41 | "new_bot": "New Bot", 42 | "new_api": "New API", 43 | "new_chat": "New Chat", 44 | "api_url": "API Url", 45 | "api_key": "API Key", 46 | "api_type": "API Type", 47 | "model_list": "Model List", 48 | "select_models": "Select Models", 49 | 50 | "enter_a_name": "Please enter a name", 51 | "enter_a_title": "Please enter a title", 52 | "duplicate_bot_name": "Duplicate Bot name", 53 | "duplicate_api_name": "Duplicate API name", 54 | "complete_all_fields": "Please complete all fields", 55 | 56 | "no_model": "no model", 57 | "all_chats": "All Chats", 58 | "chat_title": "Chat Title", 59 | 60 | "default_config": "Default Config", 61 | "text_to_speech": "Text To Speech", 62 | "chat_image_compress": "Chat Image Compress", 63 | "config_import_export": "Config Import and Export", 64 | 65 | "choose_bot": "Choose Bot", 66 | "choose_api": "Choose API", 67 | "choose_model": "Choose Model", 68 | "quality": "Quality", 69 | "min_width": "Minimal Width", 70 | "min_height": "Minimal Height", 71 | "min_width_height": "Minimal Size", 72 | "export_config": "Export Config", 73 | "import_config": "Import Config", 74 | "exporting": "Exporting...", 75 | "importing": "Importing...", 76 | 77 | "empty": "Empty", 78 | "enable": "Enable", 79 | "please_input": "Please Input", 80 | "image_enable_hint": "The original image will be used if compression fails", 81 | "image_hint": "The Quality should be between 1 and 100, with lower values resulting in higher compression. Minimum Width and Minimum Height restrict image resizing. Leave these fields empty if you're unsure.", 82 | "config_hint": "To avoid export failures, it's recommended to export the configuration to the Documents directory, or create a ChatBot subdirectory within your Downloads folder.", 83 | 84 | "exported_successfully": "Exported Successfully", 85 | "imported_successfully": "Imported Successfully", 86 | "restart_app": "Please restart App to load the new settings.", 87 | "failed_to_export": "Can't write to that directory.", 88 | 89 | "generate": "Generate", 90 | "image_size": "Image Size", 91 | "image_style": "Image Style", 92 | "image_quality": "Image Quality", 93 | "base_config": "Base Config", 94 | "optional_config": "Optional Config", 95 | "image_generation": "Image Generation", 96 | "enter_prompts": "Enter your prompts", 97 | 98 | "clone_chat": "Clone Chat", 99 | "clear_chat": "Clear Chat", 100 | "chat_settings": "Chat Settings", 101 | "export_chat_as_image": "Export Image", 102 | "cloned_successfully": "Cloned Successfully", 103 | "ensure_clear_chat": "Are you sure to clear the chat?", 104 | 105 | "saved_successfully": "Saved Successfully", 106 | "copied_successfully": "Copied Successfully", 107 | "not_implemented_yet": "Not implemented yet", 108 | 109 | "empty_link": "Empty Link", 110 | "cannot_open": "Cannot Open", 111 | "invalid_max_tokens": "Invalid Max Tokens", 112 | "invalid_temperature": "Invalid Temperature", 113 | 114 | "enter_message": "Enter your message", 115 | "image_compress_failed": "Failed to comprese image", 116 | "setup_tts_first": "Set up the TTS first", 117 | "setup_api_model_first": "Set up the API and Model first", 118 | 119 | "other": "Other", 120 | "download": "Download", 121 | "up_to_date": "You are up to date", 122 | "check_for_updates": "Check for Updates", 123 | 124 | "delete_image": "Delete image", 125 | "ensure_delete_image": "Are you sure to delete the image?", 126 | 127 | "task": "Task", 128 | "document": "Document", 129 | "workspace": "Workspace", 130 | "chat_model": "Chat Model", 131 | "model_name": "Model Name", 132 | "model_avatar": "Model Avatar", 133 | "chat_model_hint": "Is it a Chat Model?", 134 | 135 | "title_generation": "Title Generation", 136 | "title_enable_hint": "If disabled, the user's message will be used as the title", 137 | "title_prompt": "Prompt", 138 | "title_prompt_hint": "Template for the title generation prompt", 139 | "title_generation_hint": "In the prompt template, The {text} placeholder will be replaced with the user's message. If unsure, leave empty to use the built-in template.", 140 | 141 | "clearing": "Clearing...", 142 | "clear_data": "Clear Data", 143 | "cleared_successfully": "Cleared Successfully", 144 | "clear_data_chat": "All chat history", 145 | "clear_data_audio": "All TTS cache files", 146 | "clear_data_image": "All generated images", 147 | 148 | "setup_vector_first": "Set up the embedding vector API and model first", 149 | "search_vector": "Embedding Vector", 150 | "search_vector_hint": "Recommended to enable", 151 | "search_timeout": "Timeout", 152 | "search_timeout_hint": "Retrieval timeout in milliseconds", 153 | "search_searxng_base": "Base URL", 154 | "search_searxng_extra": "Additional Parameters", 155 | "search_searxng_extra_help": "For example: engines=google&language=en", 156 | "search_timeout_query": "Query Timeout", 157 | "search_timeout_query_help": "Timeout duration for SearXNG requests", 158 | "search_timeout_fetch": "Fetch Timeout", 159 | "search_timeout_fetch_help": "Timeout duration for fetching web page content", 160 | 161 | "embedding_vector": "Embedding Vector", 162 | "vector_batch_size": "Batch Size", 163 | "vector_batch_size_hint": "Maximum number of chunks that can be submitted in a single request", 164 | "vector_dimensions": "Vector Dimensions", 165 | "vector_dimensions_hint": "Output dimension of the embedding vector model", 166 | "embedding_vector_info": "Batch size is limited by the API service provider. It's recommended to check and modify accordingly. Vector dimension is an advanced option and should only be filled if necessary.", 167 | "document_config": "Document Config", 168 | "chunk_n": "Number of Chunks", 169 | "chunk_n_hint": "Number of chunks to be integrated into the context", 170 | "chunk_size": "Chunk Size", 171 | "chunk_size_hint": "Maximum number of characters a single chunk can contain", 172 | "chunk_overlap": "Chunk Overlap", 173 | "chunk_overlap_hint": "Size of the overlapping portion with the previous chunk", 174 | "document_config_hint": "Documents are divided into multiple chunks. After search and comparison, the most relevant chunks will be added to the context.", 175 | 176 | "setup_searxng_first": "Set up the SearXNG first", 177 | "search_gemini_mode": "Google Search Mode", 178 | "search_general_mode": "General Mode", 179 | "web_search": "Web Search", 180 | "search_searxng": "SearXNG", 181 | "search_searxng_hint": "SearXNG instance", 182 | "search_n": "Number of Pages", 183 | "search_n_hint": "Maximum number of web pages to retrieve", 184 | "search_prompt": "Prompt", 185 | "search_prompt_hint": "Template for context synthesis", 186 | "search_prompt_info": "In the prompt template, the {pages} placeholder will be replaced with web page content, and the {text} placeholder will be replaced with the user message. If unsure, leave it empty to use the built-in template." 187 | } 188 | -------------------------------------------------------------------------------- /lib/l10n/intl_zh_CN.arb: -------------------------------------------------------------------------------- 1 | { 2 | "@@locale": "zh_CN", 3 | "title": "ChatBot", 4 | 5 | "ok": "确定", 6 | "copy": "拷贝", 7 | "edit": "编辑", 8 | "play": "播放", 9 | "source": "源码", 10 | "delete": "删除", 11 | "images": "图片", 12 | "camera": "相机", 13 | "gallery": "图库", 14 | "settings": "设置", 15 | 16 | "bot": "角色", 17 | "bots": "角色", 18 | "apis": "接口", 19 | "config": "配置", 20 | 21 | "open": "打开", 22 | "save": "保存", 23 | "reset": "重置", 24 | "error": "错误", 25 | "share": "分享", 26 | "clear": "清空", 27 | "cancel": "取消", 28 | "reanswer": "重答", 29 | "citations": "引用", 30 | 31 | "api": "接口", 32 | "model": "模型", 33 | "voice": "音色", 34 | "max_tokens": "最大输出", 35 | "temperature": "温度", 36 | "system_prompts": "系统提示词", 37 | "streaming_response": "流式响应", 38 | 39 | "link": "链接", 40 | "name": "名称", 41 | "new_bot": "新角色", 42 | "new_api": "新接口", 43 | "new_chat": "新对话", 44 | "api_url": "接口地址", 45 | "api_key": "接口密钥", 46 | "api_type": "接口类型", 47 | "model_list": "模型列表", 48 | "select_models": "选择模型", 49 | 50 | "enter_a_name": "请输入名称", 51 | "enter_a_title": "请输入标题", 52 | "duplicate_bot_name": "角色名重复", 53 | "duplicate_api_name": "接口名重复", 54 | "complete_all_fields": "请填写所有字段", 55 | 56 | "no_model": "无模型", 57 | "all_chats": "所有对话", 58 | "chat_title": "对话标题", 59 | 60 | "default_config": "默认配置", 61 | "text_to_speech": "文本转语音", 62 | "chat_image_compress": "对话图片压缩", 63 | "config_import_export": "配置导入导出", 64 | 65 | "choose_bot": "选择角色", 66 | "choose_api": "选择接口", 67 | "choose_model": "选择模型", 68 | "quality": "质量", 69 | "min_width": "最小宽度", 70 | "min_height": "最小高度", 71 | "min_width_height": "最小宽高", 72 | "export_config": "导出配置", 73 | "import_config": "导入配置", 74 | "exporting": "正在导出...", 75 | "importing": "正在导入...", 76 | 77 | "empty": "空", 78 | "enable": "启用", 79 | "please_input": "请输入", 80 | "image_enable_hint": "压缩失败则将使用原图", 81 | "image_hint": "质量范围应在 1-100,质量越低压缩率越高。最小宽度与最小高度用于限制图片缩放,如不清楚,请留空。", 82 | "config_hint": "为避免导出失败,建议将配置导出到 Documents 目录,或在 Download 下创建 ChatBot 子目录。", 83 | 84 | "exported_successfully": "导出成功", 85 | "imported_successfully": "导入成功", 86 | "restart_app": "请重启应用以加载新配置。", 87 | "failed_to_export": "无法在该目录下写入文件。", 88 | 89 | "generate": "生成", 90 | "image_size": "图像尺寸", 91 | "image_style": "图像风格", 92 | "image_quality": "图像质量", 93 | "base_config": "基本配置", 94 | "optional_config": "可选配置", 95 | "image_generation": "图像生成", 96 | "enter_prompts": "请输入提示词", 97 | 98 | "clone_chat": "复制对话", 99 | "clear_chat": "清空对话", 100 | "chat_settings": "对话设置", 101 | "export_chat_as_image": "导出图片", 102 | "cloned_successfully": "复制成功", 103 | "ensure_clear_chat": "确定要清空对话?", 104 | 105 | "saved_successfully": "保存成功", 106 | "copied_successfully": "拷贝成功", 107 | "not_implemented_yet": "还未实现", 108 | 109 | "empty_link": "空链接", 110 | "cannot_open": "无法打开", 111 | "invalid_max_tokens": "非法的最大输出", 112 | "invalid_temperature": "非法的温度", 113 | 114 | "enter_message": "输入你的消息", 115 | "image_compress_failed": "图片压缩失败", 116 | "setup_tts_first": "请先配置文本转语音", 117 | "setup_api_model_first": "请先配置接口和模型", 118 | 119 | "other": "其他", 120 | "download": "下载", 121 | "up_to_date": "已是最新版本", 122 | "check_for_updates": "检查更新", 123 | 124 | "delete_image": "删除图片", 125 | "ensure_delete_image": "确定要删除图片?", 126 | 127 | "task": "任务", 128 | "document": "文档", 129 | "workspace": "工作空间", 130 | "chat_model": "对话模型", 131 | "model_name": "模型名称", 132 | "model_avatar": "模型头像", 133 | "chat_model_hint": "是否是对话模型?", 134 | 135 | "title_generation": "标题生成", 136 | "title_enable_hint": "禁用则将以用户消息为标题", 137 | "title_prompt": "提示词", 138 | "title_prompt_hint": "用于生成标题的提示词模板", 139 | "title_generation_hint": "提示词模板中的 {text} 占位变量会被用户消息替换。如不清楚,可留空以使用内置模板。", 140 | 141 | "clearing": "清理中...", 142 | "clear_data": "清理数据", 143 | "cleared_successfully": "清理成功", 144 | "clear_data_chat": "所有的对话数据", 145 | "clear_data_audio": "所有的 TTS 缓存", 146 | "clear_data_image": "所有的图片生成结果", 147 | 148 | "setup_vector_first": "请先配置嵌入向量接口和模型", 149 | "search_vector": "嵌入向量", 150 | "search_vector_hint": "建议开启", 151 | "search_timeout": "超时时间", 152 | "search_timeout_hint": "检索的超时毫秒数", 153 | "search_searxng_base": "根地址", 154 | "search_searxng_extra": "附加参数", 155 | "search_searxng_extra_help": "例如:engines=google&language=zh", 156 | "search_timeout_query": "检索超时", 157 | "search_timeout_query_help": "请求 SearXNG 的超时时间", 158 | "search_timeout_fetch": "抓取超时", 159 | "search_timeout_fetch_help": "抓取网页内容的超时时间", 160 | 161 | "embedding_vector": "嵌入向量", 162 | "vector_batch_size": "批大小", 163 | "vector_batch_size_hint": "单次请求能提交的最大块数量", 164 | "vector_dimensions": "向量维度", 165 | "vector_dimensions_hint": "嵌入向量模型输出向量的维度", 166 | "embedding_vector_info": "批大小受限于接口服务商,建议查询后修改。向量维度为专业选项,非必要请勿填写。", 167 | "document_config": "文档配置", 168 | "chunk_n": "块数量", 169 | "chunk_n_hint": "集成到上下文中的块数量", 170 | "chunk_size": "块大小", 171 | "chunk_size_hint": "单块能包含的最大字符数", 172 | "chunk_overlap": "块重叠", 173 | "chunk_overlap_hint": "与上一块重叠部分的大小", 174 | "document_config_hint": "文档会被划分为若干块,经过搜索比较后,最合适的几个块会被补充进上下文。", 175 | 176 | "setup_searxng_first": "请先配置 SearXNG 实例", 177 | "search_gemini_mode": "Google Search 模式", 178 | "search_general_mode": "通用模式", 179 | "web_search": "联网搜索", 180 | "search_searxng": "SearXNG", 181 | "search_searxng_hint": "SearXNG 实例", 182 | "search_n": "网页数量", 183 | "search_n_hint": "检索的网页数量上限", 184 | "search_prompt": "提示词", 185 | "search_prompt_hint": "用于合成上下文的提示词模板", 186 | "search_prompt_info": "提示词模板中的 {pages} 占位变量会被网页内容替换,{text} 占位变量会被用户消息替换。如不清楚,可留空以使用内置模板。" 187 | } 188 | -------------------------------------------------------------------------------- /lib/llm/web.dart: -------------------------------------------------------------------------------- 1 | // This file is part of ChatBot. 2 | // 3 | // ChatBot is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // ChatBot is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with ChatBot. If not, see . 15 | 16 | import "package:http/http.dart"; 17 | import "package:langchain_core/documents.dart"; 18 | import "package:langchain_core/document_loaders.dart"; 19 | import "package:beautiful_soup_dart/beautiful_soup.dart"; 20 | 21 | class WebLoader extends BaseDocumentLoader { 22 | const WebLoader( 23 | this.urls, { 24 | this.client, 25 | this.requestHeaders, 26 | this.timeout = const Duration(seconds: 10), 27 | }); 28 | 29 | final Client? client; 30 | final Duration timeout; 31 | final List urls; 32 | final Map? requestHeaders; 33 | 34 | @override 35 | Future> load() async { 36 | const badDocument = Document(pageContent: ""); 37 | 38 | final docs = await Future.wait(urls.map( 39 | (it) => _scrape(it).timeout(timeout).catchError((_) => badDocument))); 40 | docs.removeWhere((it) => it.pageContent.isEmpty); 41 | 42 | return docs; 43 | } 44 | 45 | @override 46 | Stream lazyLoad() async* { 47 | for (final url in urls) { 48 | final doc = await _scrape(url); 49 | yield doc; 50 | } 51 | } 52 | 53 | Future _scrape(final String url) async { 54 | final html = await _fetchUrl(url); 55 | final soup = BeautifulSoup(html); 56 | final body = soup.body!; 57 | 58 | body.findAll("script").forEach((e) => e.extract()); 59 | body.findAll("style").forEach((e) => e.extract()); 60 | 61 | final content = body.getText(strip: true); 62 | return Document( 63 | pageContent: content, 64 | metadata: _buildMetadata(url, soup), 65 | ); 66 | } 67 | 68 | Future _fetchUrl(final String url) async { 69 | final clnt = client ?? Client(); 70 | final res = await clnt.get( 71 | Uri.parse(url), 72 | headers: requestHeaders, 73 | ); 74 | return res.body; 75 | } 76 | 77 | Map _buildMetadata( 78 | final String url, 79 | final BeautifulSoup soup, 80 | ) { 81 | final title = soup.title; 82 | final language = soup.find("html")?.getAttrValue("lang"); 83 | final description = soup 84 | .find("meta", attrs: {"name": "description"})?.getAttrValue("content"); 85 | 86 | return { 87 | "source": url, 88 | if (title != null) "title": title.text, 89 | if (language != null) "language": language, 90 | if (description != null) "description": description.trim(), 91 | }; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | // This file is part of ChatBot. 2 | // 3 | // ChatBot is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // ChatBot is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with ChatBot. If not, see . 15 | 16 | import "config.dart"; 17 | import "gen/l10n.dart"; 18 | import "chat/chat.dart"; 19 | import "image/image.dart"; 20 | import "settings/settings.dart"; 21 | import "workspace/workspace.dart"; 22 | 23 | import "package:flutter/material.dart"; 24 | import "package:flutter_riverpod/flutter_riverpod.dart"; 25 | import "package:flutter_localizations/flutter_localizations.dart"; 26 | 27 | void main() { 28 | WidgetsFlutterBinding.ensureInitialized(); 29 | Config.init().then((_) => runApp(const ProviderScope(child: App()))); 30 | } 31 | 32 | class App extends StatelessWidget { 33 | const App({super.key}); 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | return MaterialApp( 38 | title: "ChatBot", 39 | theme: lightTheme, 40 | darkTheme: darkTheme, 41 | themeMode: ThemeMode.system, 42 | routes: { 43 | "/": (context) => ChatPage(), 44 | "/image": (context) => ImagePage(), 45 | "/settings": (context) => SettingsPage(), 46 | "/workspace": (context) => WorkspacePage(), 47 | }, 48 | localizationsDelegates: const [ 49 | S.delegate, 50 | GlobalWidgetsLocalizations.delegate, 51 | GlobalMaterialLocalizations.delegate, 52 | GlobalCupertinoLocalizations.delegate, 53 | ], 54 | supportedLocales: S.delegate.supportedLocales, 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/markdown/code.dart: -------------------------------------------------------------------------------- 1 | // This file is part of ChatBot. 2 | // 3 | // ChatBot is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // ChatBot is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with ChatBot. If not, see . 15 | 16 | import "../util.dart"; 17 | import "../gen/l10n.dart"; 18 | 19 | import "package:flutter/material.dart"; 20 | import "package:markdown/markdown.dart" as md; 21 | import "package:flutter_markdown/flutter_markdown.dart"; 22 | import "package:flutter_highlighter/flutter_highlighter.dart"; 23 | import "package:flutter_highlighter/themes/atom-one-dark.dart"; 24 | import "package:flutter_highlighter/themes/atom-one-light.dart"; 25 | 26 | final codeDarkTheme = Map.of(atomOneDarkTheme) 27 | ..["root"] = TextStyle( 28 | color: Colors.white.withOpacity(0.7), 29 | backgroundColor: Colors.transparent); 30 | 31 | final codeLightTheme = Map.of(atomOneLightTheme) 32 | ..["root"] = TextStyle( 33 | color: Colors.black.withOpacity(0.7), 34 | backgroundColor: Colors.transparent); 35 | 36 | class CodeBlockBuilder extends MarkdownElementBuilder { 37 | var language = ""; 38 | final BuildContext context; 39 | 40 | CodeBlockBuilder({required this.context}); 41 | 42 | @override 43 | void visitElementBefore(md.Element element) { 44 | final code = element.children?.first; 45 | if (code is md.Element) { 46 | final lang = code.attributes["class"]; 47 | if (lang != null) language = lang.substring(9); 48 | } 49 | super.visitElementBefore(element); 50 | } 51 | 52 | @override 53 | Widget? visitText(md.Text text, TextStyle? preferredStyle) { 54 | final colorScheme = Theme.of(context).colorScheme; 55 | final theme = switch (colorScheme.brightness) { 56 | Brightness.light => codeLightTheme, 57 | Brightness.dark => codeDarkTheme, 58 | }; 59 | final content = text.textContent.trim(); 60 | 61 | return IntrinsicWidth( 62 | child: Column( 63 | crossAxisAlignment: CrossAxisAlignment.start, 64 | children: [ 65 | Card.filled( 66 | margin: EdgeInsets.zero, 67 | color: theme == codeDarkTheme 68 | ? Colors.black.withOpacity(0.3) 69 | : Colors.blueGrey.withOpacity(0.3), 70 | shape: const RoundedRectangleBorder( 71 | borderRadius: BorderRadius.zero, 72 | ), 73 | child: Row( 74 | children: [ 75 | const SizedBox(width: 16), 76 | Text(language), 77 | const Expanded(child: SizedBox()), 78 | const SizedBox(width: 8), 79 | InkWell( 80 | onTap: () => Util.copyText( 81 | context: context, 82 | text: content, 83 | ), 84 | child: Padding( 85 | padding: 86 | const EdgeInsets.symmetric(vertical: 8, horizontal: 16), 87 | child: Text( 88 | S.of(context).copy, 89 | style: Theme.of(context).textTheme.labelSmall, 90 | ), 91 | ), 92 | ), 93 | ], 94 | ), 95 | ), 96 | SingleChildScrollView( 97 | scrollDirection: Axis.horizontal, 98 | child: HighlightView( 99 | content, 100 | tabSize: 2, 101 | theme: theme, 102 | language: language, 103 | padding: const EdgeInsets.all(8), 104 | ), 105 | ), 106 | ], 107 | ), 108 | ); 109 | } 110 | } 111 | 112 | class CodeBlockBuilder2 extends MarkdownElementBuilder { 113 | var language = ""; 114 | final BuildContext context; 115 | 116 | CodeBlockBuilder2({required this.context}); 117 | 118 | @override 119 | void visitElementBefore(md.Element element) { 120 | final code = element.children?.first; 121 | if (code is md.Element) { 122 | final lang = code.attributes["class"]; 123 | if (lang != null) language = lang.substring(9); 124 | } 125 | super.visitElementBefore(element); 126 | } 127 | 128 | @override 129 | Widget? visitText(md.Text text, TextStyle? preferredStyle) { 130 | final colorScheme = Theme.of(context).colorScheme; 131 | final theme = switch (colorScheme.brightness) { 132 | Brightness.light => codeLightTheme, 133 | Brightness.dark => codeDarkTheme, 134 | }; 135 | final content = text.textContent.trim(); 136 | 137 | return IntrinsicWidth( 138 | child: Column( 139 | crossAxisAlignment: CrossAxisAlignment.start, 140 | children: [ 141 | ColoredBox( 142 | color: theme == codeDarkTheme 143 | ? Colors.black.withOpacity(0.3) 144 | : Colors.blueGrey.withOpacity(0.3), 145 | child: Row( 146 | children: [ 147 | const SizedBox(width: 16), 148 | Text(language), 149 | const Expanded(child: SizedBox()), 150 | const SizedBox(width: 8), 151 | Padding( 152 | padding: 153 | const EdgeInsets.symmetric(vertical: 8, horizontal: 16), 154 | child: Text( 155 | S.current.copy, 156 | style: Theme.of(context).textTheme.labelSmall, 157 | ), 158 | ), 159 | ], 160 | ), 161 | ), 162 | HighlightView( 163 | content, 164 | tabSize: 2, 165 | theme: theme, 166 | language: language, 167 | padding: const EdgeInsets.all(8), 168 | ), 169 | ], 170 | ), 171 | ); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /lib/markdown/latex.dart: -------------------------------------------------------------------------------- 1 | // This file is part of ChatBot. 2 | // 3 | // ChatBot is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // ChatBot is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with ChatBot. If not, see . 15 | 16 | import "package:markdown/markdown.dart"; 17 | import "package:flutter/material.dart" hide Element; 18 | import "package:flutter_math_fork/flutter_math.dart"; 19 | import "package:flutter_markdown/flutter_markdown.dart"; 20 | 21 | class LatexElementBuilder extends MarkdownElementBuilder { 22 | final TextStyle? textStyle; 23 | final double? textScaleFactor; 24 | 25 | LatexElementBuilder({ 26 | this.textStyle, 27 | this.textScaleFactor, 28 | }); 29 | 30 | @override 31 | Widget visitElementAfterWithContext( 32 | BuildContext context, 33 | Element element, 34 | TextStyle? preferredStyle, 35 | TextStyle? parentStyle, 36 | ) { 37 | final String text = element.textContent.trim(); 38 | if (text.isEmpty) return const SizedBox(); 39 | 40 | final mathStyle = switch (element.attributes["MathStyle"]) { 41 | "display" => MathStyle.display, 42 | _ => MathStyle.text, 43 | }; 44 | 45 | return SingleChildScrollView( 46 | scrollDirection: Axis.horizontal, 47 | clipBehavior: Clip.antiAlias, 48 | child: Math.tex( 49 | text, 50 | mathStyle: mathStyle, 51 | textStyle: textStyle, 52 | textScaleFactor: textScaleFactor, 53 | ), 54 | ); 55 | } 56 | } 57 | 58 | class LatexElementBuilder2 extends MarkdownElementBuilder { 59 | final TextStyle? textStyle; 60 | final double? textScaleFactor; 61 | 62 | LatexElementBuilder2({ 63 | this.textStyle, 64 | this.textScaleFactor, 65 | }); 66 | 67 | @override 68 | Widget visitElementAfterWithContext( 69 | BuildContext context, 70 | Element element, 71 | TextStyle? preferredStyle, 72 | TextStyle? parentStyle, 73 | ) { 74 | final String text = element.textContent.trim(); 75 | if (text.isEmpty) return const SizedBox(); 76 | 77 | final mathStyle = switch (element.attributes["MathStyle"]) { 78 | "display" => MathStyle.display, 79 | _ => MathStyle.text, 80 | }; 81 | 82 | return Wrap( 83 | runSpacing: 8, 84 | crossAxisAlignment: WrapCrossAlignment.center, 85 | children: Math.tex( 86 | text, 87 | mathStyle: mathStyle, 88 | textStyle: textStyle, 89 | textScaleFactor: textScaleFactor, 90 | ).texBreak().parts, 91 | ); 92 | } 93 | } 94 | 95 | class _LatexDelimiter { 96 | final String left; 97 | final String right; 98 | final bool display; 99 | 100 | const _LatexDelimiter({ 101 | required this.left, 102 | required this.right, 103 | required this.display, 104 | }); 105 | } 106 | 107 | class LatexInlineSyntax extends InlineSyntax { 108 | static const _delimiters = [ 109 | _LatexDelimiter(left: r"$$", right: r"$$", display: true), 110 | _LatexDelimiter(left: r"$", right: r"$", display: false), 111 | _LatexDelimiter(left: r"\[", right: r"\]", display: true), 112 | _LatexDelimiter(left: r"\(", right: r"\)", display: false), 113 | _LatexDelimiter(left: r"\ce{", right: "}", display: false), 114 | _LatexDelimiter(left: r"\pu{", right: "}", display: false), 115 | ]; 116 | 117 | static String _buildPattern() => _delimiters.map((d) { 118 | final right = RegExp.escape(d.right); 119 | final left = RegExp.escape(d.left); 120 | return "$left([\\s\\S]+?)$right"; 121 | }).join("|"); 122 | 123 | LatexInlineSyntax() : super(_buildPattern()); 124 | 125 | @override 126 | bool onMatch(InlineParser parser, Match match) { 127 | final fullMatch = match[0]!; 128 | 129 | final delimiter = _delimiters.firstWhere( 130 | (d) => fullMatch.startsWith(d.left) && fullMatch.endsWith(d.right), 131 | orElse: () => _delimiters[1], 132 | ); 133 | 134 | final content = fullMatch.substring( 135 | delimiter.left.length, 136 | fullMatch.length - delimiter.right.length, 137 | ); 138 | 139 | final text = Element.text("latex", content) 140 | ..attributes["MathStyle"] = delimiter.display ? "display" : "text"; 141 | 142 | parser.addNode(text); 143 | return true; 144 | } 145 | } 146 | 147 | class LatexBlockSyntax extends BlockSyntax { 148 | static final dollarPattern = RegExp(r"^\$\$\s*$"); 149 | static final bracketPattern = RegExp(r"^\\\[\s*$"); 150 | static final endDollarPattern = RegExp(r"^\$\$\s*$"); 151 | static final endBracketPattern = RegExp(r"^\\\]\s*$"); 152 | 153 | @override 154 | RegExp get pattern => RegExp(r"^\$\$|^\\\["); 155 | 156 | @override 157 | bool canParse(BlockParser parser) { 158 | return dollarPattern.hasMatch(parser.current.content) || 159 | bracketPattern.hasMatch(parser.current.content); 160 | } 161 | 162 | @override 163 | Node parse(BlockParser parser) { 164 | final lines = []; 165 | final start = parser.current.content; 166 | final isDollar = dollarPattern.hasMatch(start); 167 | 168 | parser.advance(); 169 | 170 | while (!parser.isDone) { 171 | final line = parser.current.content; 172 | if ((isDollar && endDollarPattern.hasMatch(line)) || 173 | (!isDollar && endBracketPattern.hasMatch(line))) { 174 | parser.advance(); 175 | break; 176 | } 177 | lines.add(line); 178 | parser.advance(); 179 | } 180 | 181 | final text = Element.text("latex", lines.join("\n").trim()); 182 | text.attributes["MathStyle"] = "display"; 183 | return Element("p", [text]); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /lib/markdown/util.dart: -------------------------------------------------------------------------------- 1 | // This file is part of ChatBot. 2 | // 3 | // ChatBot is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // ChatBot is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with ChatBot. If not, see . 15 | 16 | import "latex.dart"; 17 | 18 | import "package:markdown/markdown.dart"; 19 | 20 | final mdExtensionSet = ExtensionSet( 21 | [ 22 | LatexBlockSyntax(), 23 | const TableSyntax(), 24 | const FootnoteDefSyntax(), 25 | const FencedCodeBlockSyntax(), 26 | const OrderedListWithCheckboxSyntax(), 27 | const UnorderedListWithCheckboxSyntax(), 28 | ], 29 | [ 30 | InlineHtmlSyntax(), 31 | LatexInlineSyntax(), 32 | StrikethroughSyntax(), 33 | AutolinkExtensionSyntax() 34 | ], 35 | ); 36 | 37 | String markdownToText(String markdown) { 38 | final doc = Document( 39 | extensionSet: mdExtensionSet, 40 | ); 41 | final buff = StringBuffer(); 42 | final nodes = doc.parse(markdown); 43 | 44 | for (final node in nodes) { 45 | if (node is Element) { 46 | buff.write(_elementToText(node)); 47 | } 48 | } 49 | 50 | return buff.toString().trim(); 51 | } 52 | 53 | String _elementToText(Element element) { 54 | final buff = StringBuffer(); 55 | final nodes = element.children ?? []; 56 | 57 | if (element.tag == "ul") { 58 | for (final node in nodes) { 59 | if (node is Element && node.tag == "li") { 60 | buff.write(_elementToText(node)); 61 | } 62 | } 63 | } else if (element.tag == "ol") { 64 | int index = 1; 65 | for (final node in nodes) { 66 | if (node is Element && node.tag == "li") { 67 | buff.write("${index++}. ${_elementToText(node)}"); 68 | } 69 | } 70 | } else { 71 | for (final node in nodes) { 72 | if (node is Text) { 73 | buff.write(node.text); 74 | } else if (node is Element) { 75 | final tag = node.tag; 76 | if (tag == "code") continue; 77 | if (tag == "latex") continue; 78 | if (tag == "th" || tag == "td") continue; 79 | buff.write(_elementToText(node)); 80 | } 81 | buff.write("\n"); 82 | } 83 | } 84 | 85 | return buff.toString(); 86 | } 87 | -------------------------------------------------------------------------------- /lib/settings/bot.dart: -------------------------------------------------------------------------------- 1 | // This file is part of ChatBot. 2 | // 3 | // ChatBot is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // ChatBot is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with ChatBot. If not, see . 15 | 16 | import "../util.dart"; 17 | import "../config.dart"; 18 | import "../gen/l10n.dart"; 19 | 20 | import "package:flutter/material.dart"; 21 | import "package:flutter_riverpod/flutter_riverpod.dart"; 22 | 23 | final botsProvider = 24 | NotifierProvider.autoDispose(BotsNotifier.new); 25 | 26 | class BotsNotifier extends AutoDisposeNotifier { 27 | @override 28 | void build() {} 29 | void notify() => ref.notifyListeners(); 30 | } 31 | 32 | class BotsTab extends ConsumerWidget { 33 | const BotsTab({super.key}); 34 | 35 | @override 36 | Widget build(BuildContext context, WidgetRef ref) { 37 | ref.watch(botsProvider); 38 | final bots = Config.bots.keys.toList(); 39 | 40 | return Stack( 41 | children: [ 42 | ListView.separated( 43 | padding: const EdgeInsets.all(16), 44 | separatorBuilder: (context, index) => const SizedBox(height: 12), 45 | itemCount: bots.length, 46 | itemBuilder: (context, index) => Card.filled( 47 | margin: EdgeInsets.zero, 48 | child: ListTile( 49 | title: Text( 50 | bots[index], 51 | overflow: TextOverflow.ellipsis, 52 | ), 53 | leading: const Icon(Icons.smart_toy), 54 | contentPadding: const EdgeInsets.only(left: 16, right: 8), 55 | onTap: () => Navigator.of(context).push(MaterialPageRoute( 56 | builder: (context) => BotSettings(bot: bots[index]), 57 | )), 58 | shape: const RoundedRectangleBorder( 59 | borderRadius: BorderRadius.all(Radius.circular(12)), 60 | ), 61 | ), 62 | ), 63 | ), 64 | Positioned( 65 | right: 16, 66 | bottom: 16, 67 | child: FloatingActionButton.extended( 68 | heroTag: "bot", 69 | icon: const Icon(Icons.smart_toy), 70 | label: Text(S.of(context).new_bot), 71 | onPressed: () => Navigator.of(context).push(MaterialPageRoute( 72 | builder: (context) => BotSettings(), 73 | )), 74 | ), 75 | ), 76 | ], 77 | ); 78 | } 79 | } 80 | 81 | class BotSettings extends ConsumerStatefulWidget { 82 | final String? bot; 83 | 84 | const BotSettings({ 85 | super.key, 86 | this.bot, 87 | }); 88 | 89 | @override 90 | ConsumerState createState() => _BotSettingsState(); 91 | } 92 | 93 | class _BotSettingsState extends ConsumerState { 94 | bool? _stream; 95 | late final TextEditingController _nameCtrl; 96 | late final TextEditingController _maxTokensCtrl; 97 | late final TextEditingController _temperatureCtrl; 98 | late final TextEditingController _systemPromptsCtrl; 99 | 100 | @override 101 | void initState() { 102 | super.initState(); 103 | 104 | final bot = widget.bot; 105 | final config = Config.bots[bot]; 106 | 107 | _stream = config?.stream; 108 | _nameCtrl = TextEditingController(text: bot); 109 | _maxTokensCtrl = TextEditingController(text: config?.maxTokens?.toString()); 110 | _temperatureCtrl = 111 | TextEditingController(text: config?.temperature?.toString()); 112 | _systemPromptsCtrl = TextEditingController(text: config?.systemPrompts); 113 | } 114 | 115 | @override 116 | void dispose() { 117 | _systemPromptsCtrl.dispose(); 118 | _temperatureCtrl.dispose(); 119 | _maxTokensCtrl.dispose(); 120 | _nameCtrl.dispose(); 121 | super.dispose(); 122 | } 123 | 124 | @override 125 | Widget build(BuildContext context) { 126 | final bot = widget.bot; 127 | 128 | return Scaffold( 129 | appBar: AppBar( 130 | title: Text(S.of(context).bot), 131 | ), 132 | body: ListView( 133 | padding: const EdgeInsets.only(left: 16, right: 16), 134 | children: [ 135 | const SizedBox(height: 16), 136 | TextField( 137 | controller: _nameCtrl, 138 | decoration: InputDecoration( 139 | labelText: S.of(context).name, 140 | border: const OutlineInputBorder(), 141 | ), 142 | ), 143 | const SizedBox(height: 16), 144 | Row( 145 | children: [ 146 | Expanded( 147 | child: TextField( 148 | controller: _temperatureCtrl, 149 | keyboardType: TextInputType.number, 150 | decoration: InputDecoration( 151 | labelText: S.of(context).temperature, 152 | border: const OutlineInputBorder(), 153 | ), 154 | ), 155 | ), 156 | const SizedBox(width: 12), 157 | Expanded( 158 | child: TextField( 159 | controller: _maxTokensCtrl, 160 | keyboardType: TextInputType.number, 161 | decoration: InputDecoration( 162 | labelText: S.of(context).max_tokens, 163 | border: const OutlineInputBorder(), 164 | ), 165 | ), 166 | ), 167 | ], 168 | ), 169 | const SizedBox(height: 16), 170 | TextField( 171 | maxLines: 4, 172 | controller: _systemPromptsCtrl, 173 | decoration: InputDecoration( 174 | alignLabelWithHint: true, 175 | labelText: S.of(context).system_prompts, 176 | border: const OutlineInputBorder(), 177 | ), 178 | ), 179 | const SizedBox(height: 12), 180 | Row( 181 | children: [ 182 | Flexible( 183 | child: SwitchListTile( 184 | value: _stream ?? true, 185 | title: Text(S.of(context).streaming_response), 186 | onChanged: (value) => setState(() => _stream = value), 187 | contentPadding: const EdgeInsets.only(left: 8, right: 8), 188 | ), 189 | ), 190 | ], 191 | ), 192 | const SizedBox(height: 12), 193 | Row( 194 | children: [ 195 | Expanded( 196 | child: FilledButton.tonal( 197 | child: Text(S.of(context).reset), 198 | onPressed: () { 199 | _maxTokensCtrl.text = ""; 200 | _temperatureCtrl.text = ""; 201 | _systemPromptsCtrl.text = ""; 202 | setState(() => _stream = null); 203 | }, 204 | ), 205 | ), 206 | const SizedBox(width: 6), 207 | if (bot != null) ...[ 208 | const SizedBox(width: 6), 209 | Expanded( 210 | child: FilledButton( 211 | style: FilledButton.styleFrom( 212 | backgroundColor: Theme.of(context).colorScheme.error, 213 | foregroundColor: Theme.of(context).colorScheme.onError, 214 | ), 215 | child: Text(S.of(context).delete), 216 | onPressed: () async { 217 | Config.bots.remove(bot); 218 | Config.save(); 219 | 220 | ref.read(botsProvider.notifier).notify(); 221 | Navigator.of(context).pop(); 222 | }, 223 | ), 224 | ), 225 | const SizedBox(width: 6), 226 | ], 227 | const SizedBox(width: 6), 228 | Expanded( 229 | child: FilledButton( 230 | onPressed: _save, 231 | child: Text(S.of(context).save), 232 | ), 233 | ), 234 | ], 235 | ), 236 | const SizedBox(height: 16), 237 | ], 238 | ), 239 | ); 240 | } 241 | 242 | void _save() { 243 | final name = _nameCtrl.text; 244 | 245 | if (name.isEmpty) { 246 | Util.showSnackBar( 247 | context: context, 248 | content: Text(S.of(context).enter_a_name), 249 | ); 250 | return; 251 | } 252 | 253 | final bot = widget.bot; 254 | if (Config.bots.containsKey(name) && (bot == null || name != bot)) { 255 | Util.showSnackBar( 256 | context: context, 257 | content: Text(S.of(context).duplicate_bot_name), 258 | ); 259 | return; 260 | } 261 | 262 | final maxTokens = int.tryParse(_maxTokensCtrl.text); 263 | final temperature = double.tryParse(_temperatureCtrl.text); 264 | 265 | if (_maxTokensCtrl.text.isNotEmpty && maxTokens == null) { 266 | Util.showSnackBar( 267 | context: context, 268 | content: Text(S.of(context).invalid_max_tokens), 269 | ); 270 | return; 271 | } 272 | 273 | if (_temperatureCtrl.text.isNotEmpty && temperature == null) { 274 | Util.showSnackBar( 275 | context: context, 276 | content: Text(S.of(context).invalid_temperature), 277 | ); 278 | return; 279 | } 280 | 281 | if (bot != null) Config.bots.remove(bot); 282 | final text = _systemPromptsCtrl.text; 283 | final systemPrompts = text.isEmpty ? null : text; 284 | 285 | Config.bots[name] = BotConfig( 286 | stream: _stream, 287 | maxTokens: maxTokens, 288 | temperature: temperature, 289 | systemPrompts: systemPrompts, 290 | ); 291 | Config.save(); 292 | 293 | ref.read(botsProvider.notifier).notify(); 294 | Navigator.of(context).pop(); 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /lib/settings/settings.dart: -------------------------------------------------------------------------------- 1 | // This file is part of ChatBot. 2 | // 3 | // ChatBot is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // ChatBot is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with ChatBot. If not, see . 15 | 16 | import "bot.dart"; 17 | import "api.dart"; 18 | import "config.dart"; 19 | import "../gen/l10n.dart"; 20 | 21 | import "package:flutter/material.dart"; 22 | 23 | class SettingsPage extends StatefulWidget { 24 | const SettingsPage({super.key}); 25 | 26 | @override 27 | State createState() => _SettingsPageState(); 28 | } 29 | 30 | class _SettingsPageState extends State { 31 | @override 32 | Widget build(BuildContext context) { 33 | return DefaultTabController( 34 | length: 3, 35 | child: Scaffold( 36 | appBar: AppBar( 37 | title: Text(S.of(context).settings), 38 | bottom: TabBar( 39 | tabs: [ 40 | Tab(text: S.of(context).config), 41 | Tab(text: S.of(context).bots), 42 | Tab(text: S.of(context).apis), 43 | ], 44 | ), 45 | ), 46 | body: TabBarView( 47 | children: [ 48 | const ConfigTab(), 49 | const BotsTab(), 50 | const ApisTab(), 51 | ], 52 | ), 53 | ), 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/workspace/document.dart: -------------------------------------------------------------------------------- 1 | // This file is part of ChatBot. 2 | // 3 | // ChatBot is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // ChatBot is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with ChatBot. If not, see . 15 | 16 | import "../util.dart"; 17 | import "../config.dart"; 18 | import "../gen/l10n.dart"; 19 | 20 | import "package:flutter/material.dart"; 21 | import "package:flutter_riverpod/flutter_riverpod.dart"; 22 | 23 | class DocumentTab extends ConsumerStatefulWidget { 24 | const DocumentTab({super.key}); 25 | 26 | @override 27 | ConsumerState createState() => _DocumentTabState(); 28 | } 29 | 30 | class _DocumentTabState extends ConsumerState { 31 | @override 32 | Widget build(BuildContext context) { 33 | final s = S.of(context); 34 | const padding = EdgeInsets.only(left: 24, right: 24); 35 | final primaryColor = Theme.of(context).colorScheme.primary; 36 | 37 | return ListView( 38 | padding: const EdgeInsets.only(top: 16, bottom: 8), 39 | children: [ 40 | Padding( 41 | padding: padding, 42 | child: Text( 43 | s.embedding_vector, 44 | style: TextStyle(color: primaryColor), 45 | ), 46 | ), 47 | ListTile( 48 | title: Text(s.api), 49 | contentPadding: padding, 50 | subtitle: Text(Config.vector.api ?? s.empty), 51 | onTap: () async { 52 | if (Config.apis.isEmpty) return; 53 | 54 | final api = await Dialogs.select( 55 | context: context, 56 | list: Config.apis.keys.toList(), 57 | selected: Config.vector.api, 58 | title: s.choose_api, 59 | ); 60 | if (api == null) return; 61 | 62 | setState(() => Config.vector.api = api); 63 | Config.save(); 64 | }, 65 | ), 66 | const Divider(height: 1), 67 | ListTile( 68 | title: Text(s.model), 69 | contentPadding: padding, 70 | subtitle: Text(Config.vector.model ?? s.empty), 71 | onTap: () async { 72 | final models = Config.apis[Config.vector.api]?.models; 73 | if (models == null) return; 74 | 75 | final model = await Dialogs.select( 76 | context: context, 77 | selected: Config.vector.model, 78 | title: s.choose_model, 79 | list: models, 80 | ); 81 | if (model == null) return; 82 | 83 | setState(() => Config.vector.model = model); 84 | Config.save(); 85 | }, 86 | ), 87 | const Divider(height: 1), 88 | ListTile( 89 | title: Text(s.vector_batch_size), 90 | contentPadding: padding, 91 | subtitle: Text(s.vector_batch_size_hint), 92 | onTap: () async { 93 | final texts = await Dialogs.input( 94 | context: context, 95 | title: s.vector_batch_size, 96 | fields: [ 97 | InputDialogField( 98 | label: s.please_input, 99 | hint: "64", 100 | text: Config.vector.batchSize?.toString(), 101 | ), 102 | ], 103 | ); 104 | if (texts == null) return; 105 | 106 | int? value; 107 | final text = texts[0].trim(); 108 | if (text.isNotEmpty) { 109 | value = int.tryParse(text); 110 | if (value == null) return; 111 | } 112 | 113 | Config.vector.batchSize = value; 114 | Config.save(); 115 | }, 116 | ), 117 | const Divider(height: 1), 118 | ListTile( 119 | title: Text(s.vector_dimensions), 120 | contentPadding: padding, 121 | subtitle: Text(s.vector_dimensions_hint), 122 | onTap: () async { 123 | final texts = await Dialogs.input( 124 | context: context, 125 | title: s.vector_dimensions, 126 | fields: [ 127 | InputDialogField( 128 | label: s.please_input, 129 | text: Config.vector.dimensions?.toString(), 130 | ), 131 | ], 132 | ); 133 | if (texts == null) return; 134 | 135 | int? value; 136 | final text = texts[0].trim(); 137 | if (text.isNotEmpty) { 138 | value = int.tryParse(text); 139 | if (value == null) return; 140 | } 141 | 142 | Config.vector.dimensions = value; 143 | Config.save(); 144 | }, 145 | ), 146 | const SizedBox(height: 4), 147 | InfoCard(info: s.embedding_vector_info), 148 | const SizedBox(height: 16), 149 | Padding( 150 | padding: padding, 151 | child: Text( 152 | s.document_config, 153 | style: TextStyle(color: primaryColor), 154 | ), 155 | ), 156 | ListTile( 157 | title: Text(s.chunk_n), 158 | contentPadding: padding, 159 | subtitle: Text(s.chunk_n_hint), 160 | onTap: () async { 161 | final texts = await Dialogs.input( 162 | context: context, 163 | title: s.chunk_n, 164 | fields: [ 165 | InputDialogField( 166 | label: s.please_input, 167 | hint: "8", 168 | text: Config.document.n?.toString(), 169 | ), 170 | ], 171 | ); 172 | if (texts == null) return; 173 | 174 | int? value; 175 | final text = texts[0].trim(); 176 | if (text.isNotEmpty) { 177 | value = int.tryParse(text); 178 | if (value == null) return; 179 | } 180 | 181 | Config.document.n = value; 182 | Config.save(); 183 | }, 184 | ), 185 | const Divider(height: 1), 186 | ListTile( 187 | title: Text(s.chunk_size), 188 | contentPadding: padding, 189 | subtitle: Text(s.chunk_size_hint), 190 | onTap: () async { 191 | final texts = await Dialogs.input( 192 | context: context, 193 | title: s.chunk_size, 194 | fields: [ 195 | InputDialogField( 196 | label: s.please_input, 197 | hint: "2000", 198 | text: Config.document.size?.toString(), 199 | ), 200 | ], 201 | ); 202 | if (texts == null) return; 203 | 204 | int? value; 205 | final text = texts[0].trim(); 206 | if (text.isNotEmpty) { 207 | value = int.tryParse(text); 208 | if (value == null) return; 209 | } 210 | 211 | Config.document.size = value; 212 | Config.save(); 213 | }, 214 | ), 215 | const Divider(height: 1), 216 | ListTile( 217 | title: Text(s.chunk_overlap), 218 | contentPadding: padding, 219 | subtitle: Text(s.chunk_overlap_hint), 220 | onTap: () async { 221 | final texts = await Dialogs.input( 222 | context: context, 223 | title: s.chunk_overlap, 224 | fields: [ 225 | InputDialogField( 226 | label: s.please_input, 227 | hint: "100", 228 | text: Config.document.overlap?.toString(), 229 | ), 230 | ], 231 | ); 232 | if (texts == null) return; 233 | 234 | int? value; 235 | final text = texts[0].trim(); 236 | if (text.isNotEmpty) { 237 | value = int.tryParse(text); 238 | if (value == null) return; 239 | } 240 | 241 | Config.document.overlap = value; 242 | Config.save(); 243 | }, 244 | ), 245 | const SizedBox(height: 4), 246 | InfoCard(info: s.document_config_hint), 247 | const SizedBox(height: 8), 248 | ], 249 | ); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /lib/workspace/model.dart: -------------------------------------------------------------------------------- 1 | // This file is part of ChatBot. 2 | // 3 | // ChatBot is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // ChatBot is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with ChatBot. If not, see . 15 | 16 | import "../util.dart"; 17 | import "../gen/l10n.dart"; 18 | import "../chat/chat.dart"; 19 | 20 | import "package:chatbot/config.dart"; 21 | import "package:flutter/material.dart"; 22 | import "package:flutter_riverpod/flutter_riverpod.dart"; 23 | 24 | class ModelTab extends ConsumerWidget { 25 | const ModelTab({super.key}); 26 | 27 | @override 28 | Widget build(BuildContext context, WidgetRef ref) { 29 | ref.watch(chatProvider); 30 | 31 | final modelSet = {}; 32 | for (final api in Config.apis.values) { 33 | modelSet.addAll(api.models); 34 | } 35 | final modelList = modelSet.toList(); 36 | 37 | return ListView.separated( 38 | key: ValueKey(Theme.of(context).brightness), 39 | padding: const EdgeInsets.all(16), 40 | itemCount: modelList.length, 41 | itemBuilder: (context, index) { 42 | final id = modelList[index]; 43 | 44 | return Card.filled( 45 | color: Theme.of(context).colorScheme.surfaceContainer, 46 | margin: EdgeInsets.zero, 47 | child: ListTile( 48 | leading: ModelAvatar(id: id), 49 | shape: const RoundedRectangleBorder( 50 | borderRadius: BorderRadius.all(Radius.circular(12)), 51 | ), 52 | title: Text( 53 | Config.models[id]?.name ?? id, 54 | overflow: TextOverflow.ellipsis, 55 | ), 56 | subtitle: Text( 57 | id, 58 | overflow: TextOverflow.ellipsis, 59 | ), 60 | titleTextStyle: Theme.of(context).textTheme.titleMedium, 61 | subtitleTextStyle: Theme.of(context).textTheme.bodySmall, 62 | onTap: () => showModalBottomSheet( 63 | context: context, 64 | useSafeArea: true, 65 | isScrollControlled: true, 66 | builder: (context) => Padding( 67 | padding: EdgeInsets.only( 68 | bottom: MediaQuery.of(context).viewInsets.bottom, 69 | ), 70 | child: ModelSettings(id: id), 71 | ), 72 | ), 73 | ), 74 | ); 75 | }, 76 | separatorBuilder: (context, index) => const SizedBox(height: 12), 77 | ); 78 | } 79 | } 80 | 81 | class ModelSettings extends ConsumerStatefulWidget { 82 | final String id; 83 | 84 | const ModelSettings({ 85 | required this.id, 86 | super.key, 87 | }); 88 | 89 | @override 90 | ConsumerState createState() => _ModelEditorState(); 91 | } 92 | 93 | class _ModelEditorState extends ConsumerState { 94 | late String _id; 95 | late bool? _chat; 96 | late final ModelConfig? _model; 97 | late final TextEditingController _ctrl; 98 | 99 | @override 100 | void initState() { 101 | super.initState(); 102 | _id = widget.id; 103 | _model = Config.models[_id]; 104 | _chat = _model?.chat; 105 | _ctrl = TextEditingController( 106 | text: _model?.name ?? _id, 107 | ); 108 | } 109 | 110 | @override 111 | void dispose() { 112 | _ctrl.dispose(); 113 | super.dispose(); 114 | } 115 | 116 | @override 117 | Widget build(BuildContext context) { 118 | return Column( 119 | mainAxisSize: MainAxisSize.min, 120 | crossAxisAlignment: CrossAxisAlignment.start, 121 | children: [ 122 | DialogHeader(title: S.of(context).model), 123 | const Divider(height: 1), 124 | const SizedBox(height: 8), 125 | Row( 126 | children: [ 127 | const SizedBox(width: 24), 128 | Expanded( 129 | child: TextField( 130 | controller: _ctrl, 131 | decoration: InputDecoration( 132 | labelText: S.of(context).model_name, 133 | border: const UnderlineInputBorder(), 134 | ), 135 | ), 136 | ), 137 | IconButton( 138 | icon: const Icon(Icons.transform), 139 | onPressed: _idToName, 140 | ), 141 | const SizedBox(width: 12), 142 | ], 143 | ), 144 | const SizedBox(height: 8), 145 | CheckboxListTile( 146 | value: _chat ?? true, 147 | title: Text(S.of(context).chat_model), 148 | subtitle: Text(S.of(context).chat_model_hint), 149 | onChanged: (value) => setState(() => _chat = value), 150 | contentPadding: const EdgeInsets.only(left: 24, right: 16), 151 | ), 152 | const Divider(height: 1), 153 | DialogFooter( 154 | children: [ 155 | TextButton( 156 | onPressed: _save, 157 | child: Text(S.of(context).ok), 158 | ), 159 | TextButton( 160 | onPressed: Navigator.of(context).pop, 161 | child: Text(S.of(context).cancel), 162 | ), 163 | ], 164 | ), 165 | ], 166 | ); 167 | } 168 | 169 | void _idToName() { 170 | String name = _id; 171 | final slash = name.indexOf('/'); 172 | if (slash != -1) name = name.substring(slash + 1); 173 | 174 | var parts = name.split('-'); 175 | parts.removeWhere((it) => it.isEmpty); 176 | parts = 177 | parts.map((it) => "${it[0].toUpperCase()}${it.substring(1)}").toList(); 178 | 179 | _ctrl.text = parts.join(' '); 180 | } 181 | 182 | Future _save() async { 183 | final name = _ctrl.text; 184 | if (name.isEmpty) return; 185 | 186 | final chat = _chat ?? true; 187 | Config.models[_id] = ModelConfig( 188 | name: name, 189 | chat: chat, 190 | ); 191 | Config.save(); 192 | 193 | ref.read(messagesProvider.notifier).notify(); 194 | ref.read(chatProvider.notifier).notify(); 195 | if (mounted) Navigator.of(context).pop(); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /lib/workspace/task.dart: -------------------------------------------------------------------------------- 1 | // This file is part of ChatBot. 2 | // 3 | // ChatBot is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // ChatBot is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with ChatBot. If not, see . 15 | 16 | import "../util.dart"; 17 | import "../config.dart"; 18 | import "../gen/l10n.dart"; 19 | 20 | import "package:flutter/material.dart"; 21 | import "package:flutter_riverpod/flutter_riverpod.dart"; 22 | 23 | class TaskTab extends ConsumerStatefulWidget { 24 | const TaskTab({super.key}); 25 | 26 | @override 27 | ConsumerState createState() => _TaskTabState(); 28 | } 29 | 30 | class _TaskTabState extends ConsumerState { 31 | @override 32 | Widget build(BuildContext context) { 33 | final s = S.of(context); 34 | const padding = EdgeInsets.only(left: 24, right: 24); 35 | final primaryColor = Theme.of(context).colorScheme.primary; 36 | 37 | return ListView( 38 | padding: const EdgeInsets.only(top: 16, bottom: 8), 39 | children: [ 40 | Padding( 41 | padding: padding, 42 | child: Text( 43 | s.title_generation, 44 | style: TextStyle(color: primaryColor), 45 | ), 46 | ), 47 | CheckboxListTile( 48 | title: Text(s.enable), 49 | subtitle: Text(s.title_enable_hint), 50 | contentPadding: const EdgeInsets.only(left: 24, right: 16), 51 | value: Config.title.enable ?? false, 52 | onChanged: (value) { 53 | setState(() => Config.title.enable = value); 54 | Config.save(); 55 | }, 56 | ), 57 | const Divider(height: 1), 58 | ListTile( 59 | title: Text(s.api), 60 | contentPadding: padding, 61 | subtitle: Text(Config.title.api ?? s.empty), 62 | onTap: () async { 63 | if (Config.apis.isEmpty) return; 64 | 65 | final api = await Dialogs.select( 66 | context: context, 67 | list: Config.apis.keys.toList(), 68 | selected: Config.title.api, 69 | title: s.choose_api, 70 | ); 71 | if (api == null) return; 72 | 73 | setState(() => Config.title.api = api); 74 | Config.save(); 75 | }, 76 | ), 77 | const Divider(height: 1), 78 | ListTile( 79 | title: Text(s.model), 80 | contentPadding: padding, 81 | subtitle: Text(Config.title.model ?? s.empty), 82 | onTap: () async { 83 | final models = Config.apis[Config.title.api]?.models; 84 | if (models == null) return; 85 | 86 | final model = await Dialogs.select( 87 | context: context, 88 | selected: Config.title.model, 89 | title: s.choose_model, 90 | list: models, 91 | ); 92 | if (model == null) return; 93 | 94 | setState(() => Config.title.model = model); 95 | Config.save(); 96 | }, 97 | ), 98 | const Divider(height: 1), 99 | ListTile( 100 | title: Text(s.title_prompt), 101 | contentPadding: padding, 102 | subtitle: Text(s.title_prompt_hint), 103 | onTap: () async { 104 | final texts = await Dialogs.input( 105 | context: context, 106 | title: s.title_prompt, 107 | fields: [ 108 | InputDialogField( 109 | label: s.please_input, 110 | text: Config.title.prompt, 111 | maxLines: null, 112 | ), 113 | ], 114 | ); 115 | if (texts == null) return; 116 | 117 | String? prompt; 118 | final text = texts[0].trim(); 119 | if (text.isNotEmpty) prompt = text; 120 | 121 | Config.title.prompt = prompt; 122 | Config.save(); 123 | }, 124 | ), 125 | const SizedBox(height: 4), 126 | InfoCard(info: s.title_generation_hint("{text}")), 127 | const SizedBox(height: 16), 128 | Padding( 129 | padding: padding, 130 | child: Text( 131 | s.web_search, 132 | style: TextStyle(color: primaryColor), 133 | ), 134 | ), 135 | CheckboxListTile( 136 | title: Text(s.search_vector), 137 | subtitle: Text(s.search_vector_hint), 138 | contentPadding: const EdgeInsets.only(left: 24, right: 16), 139 | value: Config.search.vector ?? false, 140 | onChanged: (value) { 141 | setState(() => Config.search.vector = value); 142 | Config.save(); 143 | }, 144 | ), 145 | const Divider(height: 1), 146 | ListTile( 147 | title: Text(s.search_searxng), 148 | contentPadding: padding, 149 | subtitle: Text(s.search_searxng_hint), 150 | onTap: () async { 151 | String? base; 152 | String? extra; 153 | 154 | final searxng = Config.search.searxng; 155 | const fixedPart = "q={text}&format=json"; 156 | 157 | if (searxng != null) { 158 | final uri = Uri.parse(searxng); 159 | base = "${uri.scheme}://${uri.host}"; 160 | extra = uri.queryParameters.entries 161 | .map((it) => "${it.key}=${it.value}") 162 | .join('&'); 163 | extra = extra.replaceFirst(fixedPart, ""); 164 | if (extra.startsWith('&')) extra = extra.replaceFirst('&', ""); 165 | } 166 | 167 | final texts = await Dialogs.input( 168 | context: context, 169 | title: s.search_searxng, 170 | fields: [ 171 | InputDialogField( 172 | text: base, 173 | label: s.search_searxng_base, 174 | hint: "https://your.searxng.com", 175 | ), 176 | InputDialogField( 177 | text: extra, 178 | label: s.search_searxng_extra, 179 | help: s.search_searxng_extra_help, 180 | ), 181 | ], 182 | ); 183 | if (texts == null) return; 184 | 185 | String? newBase; 186 | String? newExtra; 187 | final text1 = texts[0].trim(); 188 | final text2 = texts[1].trim(); 189 | if (text1.isNotEmpty) newBase = text1; 190 | if (text2.isNotEmpty) newExtra = text2; 191 | 192 | String? full; 193 | if (newBase != null) { 194 | full = "$newBase/search?$fixedPart"; 195 | if (newExtra != null) full += "&$newExtra"; 196 | } 197 | 198 | Config.search.searxng = full; 199 | Config.save(); 200 | }, 201 | ), 202 | const Divider(height: 1), 203 | ListTile( 204 | title: Text(s.search_timeout), 205 | contentPadding: padding, 206 | subtitle: Text(s.search_timeout_hint), 207 | onTap: () async { 208 | final texts = await Dialogs.input( 209 | context: context, 210 | title: s.search_timeout, 211 | fields: [ 212 | InputDialogField( 213 | hint: "3000", 214 | label: s.search_timeout_query, 215 | help: s.search_timeout_query_help, 216 | text: Config.search.queryTime?.toString(), 217 | ), 218 | InputDialogField( 219 | hint: "2000", 220 | label: s.search_timeout_fetch, 221 | help: s.search_timeout_fetch_help, 222 | text: Config.search.fetchTime?.toString(), 223 | ), 224 | ], 225 | ); 226 | if (texts == null) return; 227 | 228 | int? query; 229 | int? fetch; 230 | final text1 = texts[0].trim(); 231 | final text2 = texts[1].trim(); 232 | if (text1.isNotEmpty) { 233 | query = int.tryParse(text1); 234 | if (query == null) return; 235 | } 236 | if (text2.isNotEmpty) { 237 | fetch = int.tryParse(text2); 238 | if (fetch == null) return; 239 | } 240 | 241 | Config.search.queryTime = query; 242 | Config.search.fetchTime = fetch; 243 | Config.save(); 244 | }, 245 | ), 246 | const Divider(height: 1), 247 | ListTile( 248 | title: Text(s.search_n), 249 | contentPadding: padding, 250 | subtitle: Text(s.search_n_hint), 251 | onTap: () async { 252 | final texts = await Dialogs.input( 253 | context: context, 254 | title: s.search_n, 255 | fields: [ 256 | InputDialogField( 257 | label: s.please_input, 258 | hint: "64", 259 | text: Config.search.n?.toString(), 260 | ), 261 | ], 262 | ); 263 | if (texts == null) return; 264 | 265 | int? n; 266 | final text = texts[0].trim(); 267 | if (text.isNotEmpty) { 268 | n = int.tryParse(text); 269 | if (n == null) return; 270 | } 271 | 272 | Config.search.n = n; 273 | Config.save(); 274 | }, 275 | ), 276 | const Divider(height: 1), 277 | ListTile( 278 | title: Text(s.search_prompt), 279 | contentPadding: padding, 280 | subtitle: Text(s.search_prompt_hint), 281 | onTap: () async { 282 | final texts = await Dialogs.input( 283 | context: context, 284 | title: s.search_prompt, 285 | fields: [ 286 | InputDialogField( 287 | label: s.please_input, 288 | text: Config.search.prompt, 289 | ), 290 | ], 291 | ); 292 | if (texts == null) return; 293 | 294 | String? prompt; 295 | final text = texts[0].trim(); 296 | if (text.isNotEmpty) prompt = text; 297 | 298 | Config.search.prompt = prompt; 299 | Config.save(); 300 | }, 301 | ), 302 | const SizedBox(height: 4), 303 | InfoCard(info: s.search_prompt_info("{pages}", "{text}")), 304 | const SizedBox(height: 8), 305 | ], 306 | ); 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /lib/workspace/workspace.dart: -------------------------------------------------------------------------------- 1 | // This file is part of ChatBot. 2 | // 3 | // ChatBot is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // ChatBot is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with ChatBot. If not, see . 15 | 16 | import "task.dart"; 17 | import "model.dart"; 18 | import "document.dart"; 19 | import "../gen/l10n.dart"; 20 | 21 | import "package:flutter/material.dart"; 22 | import "package:flutter_riverpod/flutter_riverpod.dart"; 23 | 24 | class WorkspacePage extends ConsumerWidget { 25 | const WorkspacePage({super.key}); 26 | 27 | @override 28 | Widget build(BuildContext context, WidgetRef ref) { 29 | return DefaultTabController( 30 | length: 3, 31 | child: Scaffold( 32 | appBar: AppBar( 33 | title: Text(S.of(context).workspace), 34 | bottom: TabBar( 35 | tabs: [ 36 | Tab(text: S.of(context).model), 37 | Tab(text: S.of(context).task), 38 | Tab(text: S.of(context).document), 39 | ], 40 | ), 41 | ), 42 | body: TabBarView( 43 | children: [ 44 | const ModelTab(), 45 | const TaskTab(), 46 | const DocumentTab(), 47 | ], 48 | ), 49 | ), 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /linux/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Project-level configuration. 2 | cmake_minimum_required(VERSION 3.10) 3 | project(runner LANGUAGES CXX) 4 | 5 | # The name of the executable created for the application. Change this to change 6 | # the on-disk name of your application. 7 | set(BINARY_NAME "chatbot") 8 | # The unique GTK application identifier for this application. See: 9 | # https://wiki.gnome.org/HowDoI/ChooseApplicationID 10 | set(APPLICATION_ID "cc.arthur63.chatbot") 11 | 12 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent 13 | # versions of CMake. 14 | cmake_policy(SET CMP0063 NEW) 15 | 16 | # Load bundled libraries from the lib/ directory relative to the binary. 17 | set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") 18 | 19 | # Root filesystem for cross-building. 20 | if(FLUTTER_TARGET_PLATFORM_SYSROOT) 21 | set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) 22 | set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) 23 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 24 | set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) 25 | set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) 26 | set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) 27 | endif() 28 | 29 | # Define build configuration options. 30 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 31 | set(CMAKE_BUILD_TYPE "Debug" CACHE 32 | STRING "Flutter build mode" FORCE) 33 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 34 | "Debug" "Profile" "Release") 35 | endif() 36 | 37 | # Compilation settings that should be applied to most targets. 38 | # 39 | # Be cautious about adding new options here, as plugins use this function by 40 | # default. In most cases, you should add new options to specific targets instead 41 | # of modifying this function. 42 | function(APPLY_STANDARD_SETTINGS TARGET) 43 | target_compile_features(${TARGET} PUBLIC cxx_std_14) 44 | target_compile_options(${TARGET} PRIVATE -Wall -Werror) 45 | target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") 46 | target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") 47 | endfunction() 48 | 49 | # Flutter library and tool build rules. 50 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 51 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 52 | 53 | # System-level dependencies. 54 | find_package(PkgConfig REQUIRED) 55 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 56 | 57 | add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") 58 | 59 | # Define the application target. To change its name, change BINARY_NAME above, 60 | # not the value here, or `flutter run` will no longer work. 61 | # 62 | # Any new source files that you add to the application should be added here. 63 | add_executable(${BINARY_NAME} 64 | "main.cc" 65 | "my_application.cc" 66 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 67 | ) 68 | 69 | # Apply the standard set of build settings. This can be removed for applications 70 | # that need different build settings. 71 | apply_standard_settings(${BINARY_NAME}) 72 | 73 | # Add dependency libraries. Add any application-specific dependencies here. 74 | target_link_libraries(${BINARY_NAME} PRIVATE flutter) 75 | target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) 76 | 77 | # Run the Flutter tool portions of the build. This must not be removed. 78 | add_dependencies(${BINARY_NAME} flutter_assemble) 79 | 80 | # Only the install-generated bundle's copy of the executable will launch 81 | # correctly, since the resources must in the right relative locations. To avoid 82 | # people trying to run the unbundled copy, put it in a subdirectory instead of 83 | # the default top-level location. 84 | set_target_properties(${BINARY_NAME} 85 | PROPERTIES 86 | RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" 87 | ) 88 | 89 | 90 | # Generated plugin build rules, which manage building the plugins and adding 91 | # them to the application. 92 | include(flutter/generated_plugins.cmake) 93 | 94 | 95 | # === Installation === 96 | # By default, "installing" just makes a relocatable bundle in the build 97 | # directory. 98 | set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") 99 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 100 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 101 | endif() 102 | 103 | # Start with a clean build bundle directory every time. 104 | install(CODE " 105 | file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") 106 | " COMPONENT Runtime) 107 | 108 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 109 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") 110 | 111 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 112 | COMPONENT Runtime) 113 | 114 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 115 | COMPONENT Runtime) 116 | 117 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 118 | COMPONENT Runtime) 119 | 120 | foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) 121 | install(FILES "${bundled_library}" 122 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 123 | COMPONENT Runtime) 124 | endforeach(bundled_library) 125 | 126 | # Copy the native assets provided by the build.dart from all packages. 127 | set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") 128 | install(DIRECTORY "${NATIVE_ASSETS_DIR}" 129 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 130 | COMPONENT Runtime) 131 | 132 | # Fully re-copy the assets directory on each build to avoid having stale files 133 | # from a previous install. 134 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 135 | install(CODE " 136 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 137 | " COMPONENT Runtime) 138 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 139 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 140 | 141 | # Install the AOT library on non-Debug builds only. 142 | if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") 143 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 144 | COMPONENT Runtime) 145 | endif() 146 | -------------------------------------------------------------------------------- /linux/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.10) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | 12 | # Serves the same purpose as list(TRANSFORM ... PREPEND ...), 13 | # which isn't available in 3.10. 14 | function(list_prepend LIST_NAME PREFIX) 15 | set(NEW_LIST "") 16 | foreach(element ${${LIST_NAME}}) 17 | list(APPEND NEW_LIST "${PREFIX}${element}") 18 | endforeach(element) 19 | set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) 20 | endfunction() 21 | 22 | # === Flutter Library === 23 | # System-level dependencies. 24 | find_package(PkgConfig REQUIRED) 25 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 26 | pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) 27 | pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) 28 | 29 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") 30 | 31 | # Published to parent scope for install step. 32 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 33 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 34 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 35 | set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) 36 | 37 | list(APPEND FLUTTER_LIBRARY_HEADERS 38 | "fl_basic_message_channel.h" 39 | "fl_binary_codec.h" 40 | "fl_binary_messenger.h" 41 | "fl_dart_project.h" 42 | "fl_engine.h" 43 | "fl_json_message_codec.h" 44 | "fl_json_method_codec.h" 45 | "fl_message_codec.h" 46 | "fl_method_call.h" 47 | "fl_method_channel.h" 48 | "fl_method_codec.h" 49 | "fl_method_response.h" 50 | "fl_plugin_registrar.h" 51 | "fl_plugin_registry.h" 52 | "fl_standard_message_codec.h" 53 | "fl_standard_method_codec.h" 54 | "fl_string_codec.h" 55 | "fl_value.h" 56 | "fl_view.h" 57 | "flutter_linux.h" 58 | ) 59 | list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") 60 | add_library(flutter INTERFACE) 61 | target_include_directories(flutter INTERFACE 62 | "${EPHEMERAL_DIR}" 63 | ) 64 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") 65 | target_link_libraries(flutter INTERFACE 66 | PkgConfig::GTK 67 | PkgConfig::GLIB 68 | PkgConfig::GIO 69 | ) 70 | add_dependencies(flutter flutter_assemble) 71 | 72 | # === Flutter tool backend === 73 | # _phony_ is a non-existent file to force this command to run every time, 74 | # since currently there's no way to get a full input/output list from the 75 | # flutter tool. 76 | add_custom_command( 77 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 78 | ${CMAKE_CURRENT_BINARY_DIR}/_phony_ 79 | COMMAND ${CMAKE_COMMAND} -E env 80 | ${FLUTTER_TOOL_ENVIRONMENT} 81 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" 82 | ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} 83 | VERBATIM 84 | ) 85 | add_custom_target(flutter_assemble DEPENDS 86 | "${FLUTTER_LIBRARY}" 87 | ${FLUTTER_LIBRARY_HEADERS} 88 | ) 89 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | void fl_register_plugins(FlPluginRegistry* registry) { 15 | g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar = 16 | fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin"); 17 | audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar); 18 | g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = 19 | fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); 20 | file_selector_plugin_register_with_registrar(file_selector_linux_registrar); 21 | g_autoptr(FlPluginRegistrar) objectbox_flutter_libs_registrar = 22 | fl_plugin_registry_get_registrar_for_plugin(registry, "ObjectboxFlutterLibsPlugin"); 23 | objectbox_flutter_libs_plugin_register_with_registrar(objectbox_flutter_libs_registrar); 24 | g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = 25 | fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); 26 | url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); 27 | } 28 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void fl_register_plugins(FlPluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | audioplayers_linux 7 | file_selector_linux 8 | objectbox_flutter_libs 9 | url_launcher_linux 10 | ) 11 | 12 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 13 | ) 14 | 15 | set(PLUGIN_BUNDLED_LIBRARIES) 16 | 17 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 18 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 19 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 20 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 21 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 22 | endforeach(plugin) 23 | 24 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 25 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 26 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 27 | endforeach(ffi_plugin) 28 | -------------------------------------------------------------------------------- /linux/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /linux/my_application.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | #include 4 | #ifdef GDK_WINDOWING_X11 5 | #include 6 | #endif 7 | 8 | #include "flutter/generated_plugin_registrant.h" 9 | 10 | struct _MyApplication { 11 | GtkApplication parent_instance; 12 | char** dart_entrypoint_arguments; 13 | }; 14 | 15 | G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) 16 | 17 | // Implements GApplication::activate. 18 | static void my_application_activate(GApplication* application) { 19 | MyApplication* self = MY_APPLICATION(application); 20 | GtkWindow* window = 21 | GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); 22 | 23 | // Use a header bar when running in GNOME as this is the common style used 24 | // by applications and is the setup most users will be using (e.g. Ubuntu 25 | // desktop). 26 | // If running on X and not using GNOME then just use a traditional title bar 27 | // in case the window manager does more exotic layout, e.g. tiling. 28 | // If running on Wayland assume the header bar will work (may need changing 29 | // if future cases occur). 30 | gboolean use_header_bar = TRUE; 31 | #ifdef GDK_WINDOWING_X11 32 | GdkScreen* screen = gtk_window_get_screen(window); 33 | if (GDK_IS_X11_SCREEN(screen)) { 34 | const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); 35 | if (g_strcmp0(wm_name, "GNOME Shell") != 0) { 36 | use_header_bar = FALSE; 37 | } 38 | } 39 | #endif 40 | if (use_header_bar) { 41 | GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); 42 | gtk_widget_show(GTK_WIDGET(header_bar)); 43 | gtk_header_bar_set_title(header_bar, "chatbot"); 44 | gtk_header_bar_set_show_close_button(header_bar, TRUE); 45 | gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); 46 | } else { 47 | gtk_window_set_title(window, "chatbot"); 48 | } 49 | 50 | gtk_window_set_default_size(window, 1280, 720); 51 | gtk_widget_show(GTK_WIDGET(window)); 52 | 53 | g_autoptr(FlDartProject) project = fl_dart_project_new(); 54 | fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); 55 | 56 | FlView* view = fl_view_new(project); 57 | gtk_widget_show(GTK_WIDGET(view)); 58 | gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); 59 | 60 | fl_register_plugins(FL_PLUGIN_REGISTRY(view)); 61 | 62 | gtk_widget_grab_focus(GTK_WIDGET(view)); 63 | } 64 | 65 | // Implements GApplication::local_command_line. 66 | static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { 67 | MyApplication* self = MY_APPLICATION(application); 68 | // Strip out the first argument as it is the binary name. 69 | self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); 70 | 71 | g_autoptr(GError) error = nullptr; 72 | if (!g_application_register(application, nullptr, &error)) { 73 | g_warning("Failed to register: %s", error->message); 74 | *exit_status = 1; 75 | return TRUE; 76 | } 77 | 78 | g_application_activate(application); 79 | *exit_status = 0; 80 | 81 | return TRUE; 82 | } 83 | 84 | // Implements GApplication::startup. 85 | static void my_application_startup(GApplication* application) { 86 | //MyApplication* self = MY_APPLICATION(object); 87 | 88 | // Perform any actions required at application startup. 89 | 90 | G_APPLICATION_CLASS(my_application_parent_class)->startup(application); 91 | } 92 | 93 | // Implements GApplication::shutdown. 94 | static void my_application_shutdown(GApplication* application) { 95 | //MyApplication* self = MY_APPLICATION(object); 96 | 97 | // Perform any actions required at application shutdown. 98 | 99 | G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); 100 | } 101 | 102 | // Implements GObject::dispose. 103 | static void my_application_dispose(GObject* object) { 104 | MyApplication* self = MY_APPLICATION(object); 105 | g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); 106 | G_OBJECT_CLASS(my_application_parent_class)->dispose(object); 107 | } 108 | 109 | static void my_application_class_init(MyApplicationClass* klass) { 110 | G_APPLICATION_CLASS(klass)->activate = my_application_activate; 111 | G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; 112 | G_APPLICATION_CLASS(klass)->startup = my_application_startup; 113 | G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; 114 | G_OBJECT_CLASS(klass)->dispose = my_application_dispose; 115 | } 116 | 117 | static void my_application_init(MyApplication* self) {} 118 | 119 | MyApplication* my_application_new() { 120 | return MY_APPLICATION(g_object_new(my_application_get_type(), 121 | "application-id", APPLICATION_ID, 122 | "flags", G_APPLICATION_NON_UNIQUE, 123 | nullptr)); 124 | } 125 | -------------------------------------------------------------------------------- /linux/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, 7 | GtkApplication) 8 | 9 | /** 10 | * my_application_new: 11 | * 12 | * Creates a new Flutter-based application. 13 | * 14 | * Returns: a new #MyApplication. 15 | */ 16 | MyApplication* my_application_new(); 17 | 18 | #endif // FLUTTER_MY_APPLICATION_H_ 19 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: chatbot 2 | version: 0.3.5+15 3 | description: ChatBot 4 | 5 | environment: 6 | sdk: ^3.5.4 7 | 8 | dependencies: 9 | flutter: 10 | sdk: flutter 11 | flutter_localizations: 12 | sdk: flutter 13 | 14 | http: ^1.2.2 15 | intl: ^0.19.0 16 | archive: ^4.0.2 17 | 18 | animate_do: ^3.3.4 19 | flutter_svg: ^2.0.16 20 | flutter_spinkit: ^5.2.1 21 | flutter_riverpod: ^2.6.1 22 | 23 | markdown: ^7.2.2 24 | flutter_math_fork: ^0.7.2 25 | flutter_markdown: ^0.7.4+2 26 | flutter_highlighter: ^0.1.1 27 | 28 | file_picker: ^8.1.4 29 | image_picker: ^1.1.2 30 | path_provider: ^2.1.5 31 | shared_preferences: ^2.3.4 32 | flutter_image_compress: ^2.3.0 33 | 34 | langchain: ^0.7.7+2 35 | langchain_core: ^0.3.6+1 36 | langchain_openai: ^0.7.3 37 | langchain_google: ^0.6.4+2 38 | langchain_community: ^0.3.3 39 | 40 | objectbox: ^4.0.3 41 | objectbox_flutter_libs: ^4.0.3 42 | 43 | screenshot: ^3.0.0 44 | share_plus: ^10.1.2 45 | url_launcher: ^6.3.1 46 | audioplayers: ^6.1.0 47 | package_info_plus: ^8.1.2 48 | beautiful_soup_dart: ^0.3.0 49 | image_gallery_saver_plus: ^3.0.5 50 | 51 | dependency_overrides: 52 | path: 1.9.1 53 | 54 | dev_dependencies: 55 | intl_utils: ^2.8.8 56 | build_runner: ^2.4.13 57 | flutter_lints: ^5.0.0 58 | objectbox_generator: ^4.0.3 59 | 60 | flutter: 61 | assets: 62 | - assets/images/ 63 | uses-material-design: true 64 | 65 | flutter_intl: 66 | enabled: true 67 | class_name: S 68 | main_locale: en 69 | arb_dir: lib/l10n 70 | output_dir: lib/gen 71 | -------------------------------------------------------------------------------- /windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /windows/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Project-level configuration. 2 | cmake_minimum_required(VERSION 3.14) 3 | project(chatbot LANGUAGES CXX) 4 | 5 | # The name of the executable created for the application. Change this to change 6 | # the on-disk name of your application. 7 | set(BINARY_NAME "chatbot") 8 | 9 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent 10 | # versions of CMake. 11 | cmake_policy(VERSION 3.14...3.25) 12 | 13 | # Define build configuration option. 14 | get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) 15 | if(IS_MULTICONFIG) 16 | set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" 17 | CACHE STRING "" FORCE) 18 | else() 19 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 20 | set(CMAKE_BUILD_TYPE "Debug" CACHE 21 | STRING "Flutter build mode" FORCE) 22 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 23 | "Debug" "Profile" "Release") 24 | endif() 25 | endif() 26 | # Define settings for the Profile build mode. 27 | set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") 28 | set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") 29 | set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") 30 | set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") 31 | 32 | # Use Unicode for all projects. 33 | add_definitions(-DUNICODE -D_UNICODE) 34 | 35 | # Compilation settings that should be applied to most targets. 36 | # 37 | # Be cautious about adding new options here, as plugins use this function by 38 | # default. In most cases, you should add new options to specific targets instead 39 | # of modifying this function. 40 | function(APPLY_STANDARD_SETTINGS TARGET) 41 | target_compile_features(${TARGET} PUBLIC cxx_std_17) 42 | target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") 43 | target_compile_options(${TARGET} PRIVATE /EHsc) 44 | target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") 45 | target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") 46 | endfunction() 47 | 48 | # Flutter library and tool build rules. 49 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 50 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 51 | 52 | # Application build; see runner/CMakeLists.txt. 53 | add_subdirectory("runner") 54 | 55 | 56 | # Generated plugin build rules, which manage building the plugins and adding 57 | # them to the application. 58 | include(flutter/generated_plugins.cmake) 59 | 60 | 61 | # === Installation === 62 | # Support files are copied into place next to the executable, so that it can 63 | # run in place. This is done instead of making a separate bundle (as on Linux) 64 | # so that building and running from within Visual Studio will work. 65 | set(BUILD_BUNDLE_DIR "$") 66 | # Make the "install" step default, as it's required to run. 67 | set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) 68 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 69 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 70 | endif() 71 | 72 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 73 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") 74 | 75 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 76 | COMPONENT Runtime) 77 | 78 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 79 | COMPONENT Runtime) 80 | 81 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 82 | COMPONENT Runtime) 83 | 84 | if(PLUGIN_BUNDLED_LIBRARIES) 85 | install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" 86 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 87 | COMPONENT Runtime) 88 | endif() 89 | 90 | # Copy the native assets provided by the build.dart from all packages. 91 | set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") 92 | install(DIRECTORY "${NATIVE_ASSETS_DIR}" 93 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 94 | COMPONENT Runtime) 95 | 96 | # Fully re-copy the assets directory on each build to avoid having stale files 97 | # from a previous install. 98 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 99 | install(CODE " 100 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 101 | " COMPONENT Runtime) 102 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 103 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 104 | 105 | # Install the AOT library on non-Debug builds only. 106 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 107 | CONFIGURATIONS Profile;Release 108 | COMPONENT Runtime) 109 | -------------------------------------------------------------------------------- /windows/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.14) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") 12 | 13 | # Set fallback configurations for older versions of the flutter tool. 14 | if (NOT DEFINED FLUTTER_TARGET_PLATFORM) 15 | set(FLUTTER_TARGET_PLATFORM "windows-x64") 16 | endif() 17 | 18 | # === Flutter Library === 19 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") 20 | 21 | # Published to parent scope for install step. 22 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 23 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 24 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 25 | set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) 26 | 27 | list(APPEND FLUTTER_LIBRARY_HEADERS 28 | "flutter_export.h" 29 | "flutter_windows.h" 30 | "flutter_messenger.h" 31 | "flutter_plugin_registrar.h" 32 | "flutter_texture_registrar.h" 33 | ) 34 | list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") 35 | add_library(flutter INTERFACE) 36 | target_include_directories(flutter INTERFACE 37 | "${EPHEMERAL_DIR}" 38 | ) 39 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") 40 | add_dependencies(flutter flutter_assemble) 41 | 42 | # === Wrapper === 43 | list(APPEND CPP_WRAPPER_SOURCES_CORE 44 | "core_implementations.cc" 45 | "standard_codec.cc" 46 | ) 47 | list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") 48 | list(APPEND CPP_WRAPPER_SOURCES_PLUGIN 49 | "plugin_registrar.cc" 50 | ) 51 | list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") 52 | list(APPEND CPP_WRAPPER_SOURCES_APP 53 | "flutter_engine.cc" 54 | "flutter_view_controller.cc" 55 | ) 56 | list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") 57 | 58 | # Wrapper sources needed for a plugin. 59 | add_library(flutter_wrapper_plugin STATIC 60 | ${CPP_WRAPPER_SOURCES_CORE} 61 | ${CPP_WRAPPER_SOURCES_PLUGIN} 62 | ) 63 | apply_standard_settings(flutter_wrapper_plugin) 64 | set_target_properties(flutter_wrapper_plugin PROPERTIES 65 | POSITION_INDEPENDENT_CODE ON) 66 | set_target_properties(flutter_wrapper_plugin PROPERTIES 67 | CXX_VISIBILITY_PRESET hidden) 68 | target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) 69 | target_include_directories(flutter_wrapper_plugin PUBLIC 70 | "${WRAPPER_ROOT}/include" 71 | ) 72 | add_dependencies(flutter_wrapper_plugin flutter_assemble) 73 | 74 | # Wrapper sources needed for the runner. 75 | add_library(flutter_wrapper_app STATIC 76 | ${CPP_WRAPPER_SOURCES_CORE} 77 | ${CPP_WRAPPER_SOURCES_APP} 78 | ) 79 | apply_standard_settings(flutter_wrapper_app) 80 | target_link_libraries(flutter_wrapper_app PUBLIC flutter) 81 | target_include_directories(flutter_wrapper_app PUBLIC 82 | "${WRAPPER_ROOT}/include" 83 | ) 84 | add_dependencies(flutter_wrapper_app flutter_assemble) 85 | 86 | # === Flutter tool backend === 87 | # _phony_ is a non-existent file to force this command to run every time, 88 | # since currently there's no way to get a full input/output list from the 89 | # flutter tool. 90 | set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") 91 | set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) 92 | add_custom_command( 93 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 94 | ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} 95 | ${CPP_WRAPPER_SOURCES_APP} 96 | ${PHONY_OUTPUT} 97 | COMMAND ${CMAKE_COMMAND} -E env 98 | ${FLUTTER_TOOL_ENVIRONMENT} 99 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" 100 | ${FLUTTER_TARGET_PLATFORM} $ 101 | VERBATIM 102 | ) 103 | add_custom_target(flutter_assemble DEPENDS 104 | "${FLUTTER_LIBRARY}" 105 | ${FLUTTER_LIBRARY_HEADERS} 106 | ${CPP_WRAPPER_SOURCES_CORE} 107 | ${CPP_WRAPPER_SOURCES_PLUGIN} 108 | ${CPP_WRAPPER_SOURCES_APP} 109 | ) 110 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | void RegisterPlugins(flutter::PluginRegistry* registry) { 16 | AudioplayersWindowsPluginRegisterWithRegistrar( 17 | registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); 18 | FileSelectorWindowsRegisterWithRegistrar( 19 | registry->GetRegistrarForPlugin("FileSelectorWindows")); 20 | ObjectboxFlutterLibsPluginRegisterWithRegistrar( 21 | registry->GetRegistrarForPlugin("ObjectboxFlutterLibsPlugin")); 22 | SharePlusWindowsPluginCApiRegisterWithRegistrar( 23 | registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); 24 | UrlLauncherWindowsRegisterWithRegistrar( 25 | registry->GetRegistrarForPlugin("UrlLauncherWindows")); 26 | } 27 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | audioplayers_windows 7 | file_selector_windows 8 | objectbox_flutter_libs 9 | share_plus 10 | url_launcher_windows 11 | ) 12 | 13 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 14 | ) 15 | 16 | set(PLUGIN_BUNDLED_LIBRARIES) 17 | 18 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 19 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 20 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 21 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 22 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 23 | endforeach(plugin) 24 | 25 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 26 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 27 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 28 | endforeach(ffi_plugin) 29 | -------------------------------------------------------------------------------- /windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} WIN32 10 | "flutter_window.cpp" 11 | "main.cpp" 12 | "utils.cpp" 13 | "win32_window.cpp" 14 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 15 | "Runner.rc" 16 | "runner.exe.manifest" 17 | ) 18 | 19 | # Apply the standard set of build settings. This can be removed for applications 20 | # that need different build settings. 21 | apply_standard_settings(${BINARY_NAME}) 22 | 23 | # Add preprocessor definitions for the build version. 24 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") 25 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") 26 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") 27 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") 28 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") 29 | 30 | # Disable Windows macros that collide with C++ standard library functions. 31 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 32 | 33 | # Add dependency libraries and include directories. Add any application-specific 34 | # dependencies here. 35 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 36 | target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") 37 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 38 | 39 | # Run the Flutter tool portions of the build. This must not be removed. 40 | add_dependencies(${BINARY_NAME} flutter_assemble) 41 | -------------------------------------------------------------------------------- /windows/runner/Runner.rc: -------------------------------------------------------------------------------- 1 | // Microsoft Visual C++ generated resource script. 2 | // 3 | #pragma code_page(65001) 4 | #include "resource.h" 5 | 6 | #define APSTUDIO_READONLY_SYMBOLS 7 | ///////////////////////////////////////////////////////////////////////////// 8 | // 9 | // Generated from the TEXTINCLUDE 2 resource. 10 | // 11 | #include "winres.h" 12 | 13 | ///////////////////////////////////////////////////////////////////////////// 14 | #undef APSTUDIO_READONLY_SYMBOLS 15 | 16 | ///////////////////////////////////////////////////////////////////////////// 17 | // English (United States) resources 18 | 19 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) 20 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US 21 | 22 | #ifdef APSTUDIO_INVOKED 23 | ///////////////////////////////////////////////////////////////////////////// 24 | // 25 | // TEXTINCLUDE 26 | // 27 | 28 | 1 TEXTINCLUDE 29 | BEGIN 30 | "resource.h\0" 31 | END 32 | 33 | 2 TEXTINCLUDE 34 | BEGIN 35 | "#include ""winres.h""\r\n" 36 | "\0" 37 | END 38 | 39 | 3 TEXTINCLUDE 40 | BEGIN 41 | "\r\n" 42 | "\0" 43 | END 44 | 45 | #endif // APSTUDIO_INVOKED 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////////// 49 | // 50 | // Icon 51 | // 52 | 53 | // Icon with lowest ID value placed first to ensure application icon 54 | // remains consistent on all systems. 55 | IDI_APP_ICON ICON "resources\\app_icon.ico" 56 | 57 | 58 | ///////////////////////////////////////////////////////////////////////////// 59 | // 60 | // Version 61 | // 62 | 63 | #if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) 64 | #define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD 65 | #else 66 | #define VERSION_AS_NUMBER 1,0,0,0 67 | #endif 68 | 69 | #if defined(FLUTTER_VERSION) 70 | #define VERSION_AS_STRING FLUTTER_VERSION 71 | #else 72 | #define VERSION_AS_STRING "1.0.0" 73 | #endif 74 | 75 | VS_VERSION_INFO VERSIONINFO 76 | FILEVERSION VERSION_AS_NUMBER 77 | PRODUCTVERSION VERSION_AS_NUMBER 78 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 79 | #ifdef _DEBUG 80 | FILEFLAGS VS_FF_DEBUG 81 | #else 82 | FILEFLAGS 0x0L 83 | #endif 84 | FILEOS VOS__WINDOWS32 85 | FILETYPE VFT_APP 86 | FILESUBTYPE 0x0L 87 | BEGIN 88 | BLOCK "StringFileInfo" 89 | BEGIN 90 | BLOCK "040904e4" 91 | BEGIN 92 | VALUE "CompanyName", "cc.arthur63" "\0" 93 | VALUE "FileDescription", "chatbot" "\0" 94 | VALUE "FileVersion", VERSION_AS_STRING "\0" 95 | VALUE "InternalName", "chatbot" "\0" 96 | VALUE "LegalCopyright", "Copyright (C) 2024 cc.arthur63. All rights reserved." "\0" 97 | VALUE "OriginalFilename", "chatbot.exe" "\0" 98 | VALUE "ProductName", "chatbot" "\0" 99 | VALUE "ProductVersion", VERSION_AS_STRING "\0" 100 | END 101 | END 102 | BLOCK "VarFileInfo" 103 | BEGIN 104 | VALUE "Translation", 0x409, 1252 105 | END 106 | END 107 | 108 | #endif // English (United States) resources 109 | ///////////////////////////////////////////////////////////////////////////// 110 | 111 | 112 | 113 | #ifndef APSTUDIO_INVOKED 114 | ///////////////////////////////////////////////////////////////////////////// 115 | // 116 | // Generated from the TEXTINCLUDE 3 resource. 117 | // 118 | 119 | 120 | ///////////////////////////////////////////////////////////////////////////// 121 | #endif // not APSTUDIO_INVOKED 122 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.cpp: -------------------------------------------------------------------------------- 1 | #include "flutter_window.h" 2 | 3 | #include 4 | 5 | #include "flutter/generated_plugin_registrant.h" 6 | 7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project) 8 | : project_(project) {} 9 | 10 | FlutterWindow::~FlutterWindow() {} 11 | 12 | bool FlutterWindow::OnCreate() { 13 | if (!Win32Window::OnCreate()) { 14 | return false; 15 | } 16 | 17 | RECT frame = GetClientArea(); 18 | 19 | // The size here must match the window dimensions to avoid unnecessary surface 20 | // creation / destruction in the startup path. 21 | flutter_controller_ = std::make_unique( 22 | frame.right - frame.left, frame.bottom - frame.top, project_); 23 | // Ensure that basic setup of the controller was successful. 24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) { 25 | return false; 26 | } 27 | RegisterPlugins(flutter_controller_->engine()); 28 | SetChildContent(flutter_controller_->view()->GetNativeWindow()); 29 | 30 | flutter_controller_->engine()->SetNextFrameCallback([&]() { 31 | this->Show(); 32 | }); 33 | 34 | // Flutter can complete the first frame before the "show window" callback is 35 | // registered. The following call ensures a frame is pending to ensure the 36 | // window is shown. It is a no-op if the first frame hasn't completed yet. 37 | flutter_controller_->ForceRedraw(); 38 | 39 | return true; 40 | } 41 | 42 | void FlutterWindow::OnDestroy() { 43 | if (flutter_controller_) { 44 | flutter_controller_ = nullptr; 45 | } 46 | 47 | Win32Window::OnDestroy(); 48 | } 49 | 50 | LRESULT 51 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 52 | WPARAM const wparam, 53 | LPARAM const lparam) noexcept { 54 | // Give Flutter, including plugins, an opportunity to handle window messages. 55 | if (flutter_controller_) { 56 | std::optional result = 57 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 58 | lparam); 59 | if (result) { 60 | return *result; 61 | } 62 | } 63 | 64 | switch (message) { 65 | case WM_FONTCHANGE: 66 | flutter_controller_->engine()->ReloadSystemFonts(); 67 | break; 68 | } 69 | 70 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 71 | } 72 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | }; 32 | 33 | #endif // RUNNER_FLUTTER_WINDOW_H_ 34 | -------------------------------------------------------------------------------- /windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "utils.h" 7 | 8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 9 | _In_ wchar_t *command_line, _In_ int show_command) { 10 | // Attach to console when present (e.g., 'flutter run') or create a 11 | // new console when running with a debugger. 12 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 13 | CreateAndAttachConsole(); 14 | } 15 | 16 | // Initialize COM, so that it is available for use in the library and/or 17 | // plugins. 18 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 19 | 20 | flutter::DartProject project(L"data"); 21 | 22 | std::vector command_line_arguments = 23 | GetCommandLineArguments(); 24 | 25 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 26 | 27 | FlutterWindow window(project); 28 | Win32Window::Point origin(10, 10); 29 | Win32Window::Size size(1280, 720); 30 | if (!window.Create(L"chatbot", origin, size)) { 31 | return EXIT_FAILURE; 32 | } 33 | window.SetQuitOnClose(true); 34 | 35 | ::MSG msg; 36 | while (::GetMessage(&msg, nullptr, 0, 0)) { 37 | ::TranslateMessage(&msg); 38 | ::DispatchMessage(&msg); 39 | } 40 | 41 | ::CoUninitialize(); 42 | return EXIT_SUCCESS; 43 | } 44 | -------------------------------------------------------------------------------- /windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fanenr/flutter-chatbot/b2508200fc4cc68d20249044d28b2e3fb73e241a/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | unsigned int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr) 51 | -1; // remove the trailing null character 52 | int input_length = (int)wcslen(utf16_string); 53 | std::string utf8_string; 54 | if (target_length == 0 || target_length > utf8_string.max_size()) { 55 | return utf8_string; 56 | } 57 | utf8_string.resize(target_length); 58 | int converted_length = ::WideCharToMultiByte( 59 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 60 | input_length, utf8_string.data(), target_length, nullptr, nullptr); 61 | if (converted_length == 0) { 62 | return std::string(); 63 | } 64 | return utf8_string; 65 | } 66 | -------------------------------------------------------------------------------- /windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | -------------------------------------------------------------------------------- /windows/runner/win32_window.cpp: -------------------------------------------------------------------------------- 1 | #include "win32_window.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "resource.h" 7 | 8 | namespace { 9 | 10 | /// Window attribute that enables dark mode window decorations. 11 | /// 12 | /// Redefined in case the developer's machine has a Windows SDK older than 13 | /// version 10.0.22000.0. 14 | /// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute 15 | #ifndef DWMWA_USE_IMMERSIVE_DARK_MODE 16 | #define DWMWA_USE_IMMERSIVE_DARK_MODE 20 17 | #endif 18 | 19 | constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; 20 | 21 | /// Registry key for app theme preference. 22 | /// 23 | /// A value of 0 indicates apps should use dark mode. A non-zero or missing 24 | /// value indicates apps should use light mode. 25 | constexpr const wchar_t kGetPreferredBrightnessRegKey[] = 26 | L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; 27 | constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; 28 | 29 | // The number of Win32Window objects that currently exist. 30 | static int g_active_window_count = 0; 31 | 32 | using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); 33 | 34 | // Scale helper to convert logical scaler values to physical using passed in 35 | // scale factor 36 | int Scale(int source, double scale_factor) { 37 | return static_cast(source * scale_factor); 38 | } 39 | 40 | // Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. 41 | // This API is only needed for PerMonitor V1 awareness mode. 42 | void EnableFullDpiSupportIfAvailable(HWND hwnd) { 43 | HMODULE user32_module = LoadLibraryA("User32.dll"); 44 | if (!user32_module) { 45 | return; 46 | } 47 | auto enable_non_client_dpi_scaling = 48 | reinterpret_cast( 49 | GetProcAddress(user32_module, "EnableNonClientDpiScaling")); 50 | if (enable_non_client_dpi_scaling != nullptr) { 51 | enable_non_client_dpi_scaling(hwnd); 52 | } 53 | FreeLibrary(user32_module); 54 | } 55 | 56 | } // namespace 57 | 58 | // Manages the Win32Window's window class registration. 59 | class WindowClassRegistrar { 60 | public: 61 | ~WindowClassRegistrar() = default; 62 | 63 | // Returns the singleton registrar instance. 64 | static WindowClassRegistrar* GetInstance() { 65 | if (!instance_) { 66 | instance_ = new WindowClassRegistrar(); 67 | } 68 | return instance_; 69 | } 70 | 71 | // Returns the name of the window class, registering the class if it hasn't 72 | // previously been registered. 73 | const wchar_t* GetWindowClass(); 74 | 75 | // Unregisters the window class. Should only be called if there are no 76 | // instances of the window. 77 | void UnregisterWindowClass(); 78 | 79 | private: 80 | WindowClassRegistrar() = default; 81 | 82 | static WindowClassRegistrar* instance_; 83 | 84 | bool class_registered_ = false; 85 | }; 86 | 87 | WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; 88 | 89 | const wchar_t* WindowClassRegistrar::GetWindowClass() { 90 | if (!class_registered_) { 91 | WNDCLASS window_class{}; 92 | window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); 93 | window_class.lpszClassName = kWindowClassName; 94 | window_class.style = CS_HREDRAW | CS_VREDRAW; 95 | window_class.cbClsExtra = 0; 96 | window_class.cbWndExtra = 0; 97 | window_class.hInstance = GetModuleHandle(nullptr); 98 | window_class.hIcon = 99 | LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); 100 | window_class.hbrBackground = 0; 101 | window_class.lpszMenuName = nullptr; 102 | window_class.lpfnWndProc = Win32Window::WndProc; 103 | RegisterClass(&window_class); 104 | class_registered_ = true; 105 | } 106 | return kWindowClassName; 107 | } 108 | 109 | void WindowClassRegistrar::UnregisterWindowClass() { 110 | UnregisterClass(kWindowClassName, nullptr); 111 | class_registered_ = false; 112 | } 113 | 114 | Win32Window::Win32Window() { 115 | ++g_active_window_count; 116 | } 117 | 118 | Win32Window::~Win32Window() { 119 | --g_active_window_count; 120 | Destroy(); 121 | } 122 | 123 | bool Win32Window::Create(const std::wstring& title, 124 | const Point& origin, 125 | const Size& size) { 126 | Destroy(); 127 | 128 | const wchar_t* window_class = 129 | WindowClassRegistrar::GetInstance()->GetWindowClass(); 130 | 131 | const POINT target_point = {static_cast(origin.x), 132 | static_cast(origin.y)}; 133 | HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); 134 | UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); 135 | double scale_factor = dpi / 96.0; 136 | 137 | HWND window = CreateWindow( 138 | window_class, title.c_str(), WS_OVERLAPPEDWINDOW, 139 | Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), 140 | Scale(size.width, scale_factor), Scale(size.height, scale_factor), 141 | nullptr, nullptr, GetModuleHandle(nullptr), this); 142 | 143 | if (!window) { 144 | return false; 145 | } 146 | 147 | UpdateTheme(window); 148 | 149 | return OnCreate(); 150 | } 151 | 152 | bool Win32Window::Show() { 153 | return ShowWindow(window_handle_, SW_SHOWNORMAL); 154 | } 155 | 156 | // static 157 | LRESULT CALLBACK Win32Window::WndProc(HWND const window, 158 | UINT const message, 159 | WPARAM const wparam, 160 | LPARAM const lparam) noexcept { 161 | if (message == WM_NCCREATE) { 162 | auto window_struct = reinterpret_cast(lparam); 163 | SetWindowLongPtr(window, GWLP_USERDATA, 164 | reinterpret_cast(window_struct->lpCreateParams)); 165 | 166 | auto that = static_cast(window_struct->lpCreateParams); 167 | EnableFullDpiSupportIfAvailable(window); 168 | that->window_handle_ = window; 169 | } else if (Win32Window* that = GetThisFromHandle(window)) { 170 | return that->MessageHandler(window, message, wparam, lparam); 171 | } 172 | 173 | return DefWindowProc(window, message, wparam, lparam); 174 | } 175 | 176 | LRESULT 177 | Win32Window::MessageHandler(HWND hwnd, 178 | UINT const message, 179 | WPARAM const wparam, 180 | LPARAM const lparam) noexcept { 181 | switch (message) { 182 | case WM_DESTROY: 183 | window_handle_ = nullptr; 184 | Destroy(); 185 | if (quit_on_close_) { 186 | PostQuitMessage(0); 187 | } 188 | return 0; 189 | 190 | case WM_DPICHANGED: { 191 | auto newRectSize = reinterpret_cast(lparam); 192 | LONG newWidth = newRectSize->right - newRectSize->left; 193 | LONG newHeight = newRectSize->bottom - newRectSize->top; 194 | 195 | SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, 196 | newHeight, SWP_NOZORDER | SWP_NOACTIVATE); 197 | 198 | return 0; 199 | } 200 | case WM_SIZE: { 201 | RECT rect = GetClientArea(); 202 | if (child_content_ != nullptr) { 203 | // Size and position the child window. 204 | MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, 205 | rect.bottom - rect.top, TRUE); 206 | } 207 | return 0; 208 | } 209 | 210 | case WM_ACTIVATE: 211 | if (child_content_ != nullptr) { 212 | SetFocus(child_content_); 213 | } 214 | return 0; 215 | 216 | case WM_DWMCOLORIZATIONCOLORCHANGED: 217 | UpdateTheme(hwnd); 218 | return 0; 219 | } 220 | 221 | return DefWindowProc(window_handle_, message, wparam, lparam); 222 | } 223 | 224 | void Win32Window::Destroy() { 225 | OnDestroy(); 226 | 227 | if (window_handle_) { 228 | DestroyWindow(window_handle_); 229 | window_handle_ = nullptr; 230 | } 231 | if (g_active_window_count == 0) { 232 | WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); 233 | } 234 | } 235 | 236 | Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { 237 | return reinterpret_cast( 238 | GetWindowLongPtr(window, GWLP_USERDATA)); 239 | } 240 | 241 | void Win32Window::SetChildContent(HWND content) { 242 | child_content_ = content; 243 | SetParent(content, window_handle_); 244 | RECT frame = GetClientArea(); 245 | 246 | MoveWindow(content, frame.left, frame.top, frame.right - frame.left, 247 | frame.bottom - frame.top, true); 248 | 249 | SetFocus(child_content_); 250 | } 251 | 252 | RECT Win32Window::GetClientArea() { 253 | RECT frame; 254 | GetClientRect(window_handle_, &frame); 255 | return frame; 256 | } 257 | 258 | HWND Win32Window::GetHandle() { 259 | return window_handle_; 260 | } 261 | 262 | void Win32Window::SetQuitOnClose(bool quit_on_close) { 263 | quit_on_close_ = quit_on_close; 264 | } 265 | 266 | bool Win32Window::OnCreate() { 267 | // No-op; provided for subclasses. 268 | return true; 269 | } 270 | 271 | void Win32Window::OnDestroy() { 272 | // No-op; provided for subclasses. 273 | } 274 | 275 | void Win32Window::UpdateTheme(HWND const window) { 276 | DWORD light_mode; 277 | DWORD light_mode_size = sizeof(light_mode); 278 | LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, 279 | kGetPreferredBrightnessRegValue, 280 | RRF_RT_REG_DWORD, nullptr, &light_mode, 281 | &light_mode_size); 282 | 283 | if (result == ERROR_SUCCESS) { 284 | BOOL enable_dark_mode = light_mode == 0; 285 | DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, 286 | &enable_dark_mode, sizeof(enable_dark_mode)); 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /windows/runner/win32_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_WIN32_WINDOW_H_ 2 | #define RUNNER_WIN32_WINDOW_H_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | // A class abstraction for a high DPI-aware Win32 Window. Intended to be 11 | // inherited from by classes that wish to specialize with custom 12 | // rendering and input handling 13 | class Win32Window { 14 | public: 15 | struct Point { 16 | unsigned int x; 17 | unsigned int y; 18 | Point(unsigned int x, unsigned int y) : x(x), y(y) {} 19 | }; 20 | 21 | struct Size { 22 | unsigned int width; 23 | unsigned int height; 24 | Size(unsigned int width, unsigned int height) 25 | : width(width), height(height) {} 26 | }; 27 | 28 | Win32Window(); 29 | virtual ~Win32Window(); 30 | 31 | // Creates a win32 window with |title| that is positioned and sized using 32 | // |origin| and |size|. New windows are created on the default monitor. Window 33 | // sizes are specified to the OS in physical pixels, hence to ensure a 34 | // consistent size this function will scale the inputted width and height as 35 | // as appropriate for the default monitor. The window is invisible until 36 | // |Show| is called. Returns true if the window was created successfully. 37 | bool Create(const std::wstring& title, const Point& origin, const Size& size); 38 | 39 | // Show the current window. Returns true if the window was successfully shown. 40 | bool Show(); 41 | 42 | // Release OS resources associated with window. 43 | void Destroy(); 44 | 45 | // Inserts |content| into the window tree. 46 | void SetChildContent(HWND content); 47 | 48 | // Returns the backing Window handle to enable clients to set icon and other 49 | // window properties. Returns nullptr if the window has been destroyed. 50 | HWND GetHandle(); 51 | 52 | // If true, closing this window will quit the application. 53 | void SetQuitOnClose(bool quit_on_close); 54 | 55 | // Return a RECT representing the bounds of the current client area. 56 | RECT GetClientArea(); 57 | 58 | protected: 59 | // Processes and route salient window messages for mouse handling, 60 | // size change and DPI. Delegates handling of these to member overloads that 61 | // inheriting classes can handle. 62 | virtual LRESULT MessageHandler(HWND window, 63 | UINT const message, 64 | WPARAM const wparam, 65 | LPARAM const lparam) noexcept; 66 | 67 | // Called when CreateAndShow is called, allowing subclass window-related 68 | // setup. Subclasses should return false if setup fails. 69 | virtual bool OnCreate(); 70 | 71 | // Called when Destroy is called. 72 | virtual void OnDestroy(); 73 | 74 | private: 75 | friend class WindowClassRegistrar; 76 | 77 | // OS callback called by message pump. Handles the WM_NCCREATE message which 78 | // is passed when the non-client area is being created and enables automatic 79 | // non-client DPI scaling so that the non-client area automatically 80 | // responds to changes in DPI. All other messages are handled by 81 | // MessageHandler. 82 | static LRESULT CALLBACK WndProc(HWND const window, 83 | UINT const message, 84 | WPARAM const wparam, 85 | LPARAM const lparam) noexcept; 86 | 87 | // Retrieves a class instance pointer for |window| 88 | static Win32Window* GetThisFromHandle(HWND const window) noexcept; 89 | 90 | // Update the window frame's theme to match the system theme. 91 | static void UpdateTheme(HWND const window); 92 | 93 | bool quit_on_close_ = false; 94 | 95 | // window handle for top level window. 96 | HWND window_handle_ = nullptr; 97 | 98 | // window handle for hosted content. 99 | HWND child_content_ = nullptr; 100 | }; 101 | 102 | #endif // RUNNER_WIN32_WINDOW_H_ 103 | --------------------------------------------------------------------------------