├── lib └── chat-app │ ├── test.dart │ ├── utils │ ├── PackageValue.dart │ ├── FileUtils.dart │ ├── chat │ │ ├── goto_chat.dart │ │ └── token_calc.dart │ ├── json_util.dart │ ├── lorebooks │ │ └── memory_utils.dart │ ├── sillyTavern │ │ ├── STRegexImporter.dart │ │ └── STLorebookImporter.dart │ ├── markdown │ │ ├── latex_block_syntax.dart │ │ ├── latex_element_builder.dart │ │ └── latex_inline_syntax.dart │ ├── service_handlers │ │ ├── ServiceHandler.dart │ │ ├── KimiServiceHandler.dart │ │ ├── DeepSeekServiceHandler.dart │ │ ├── ServiceHandlerFactory.dart │ │ └── SiliconFlowServiceHandler.dart │ ├── customNav.dart │ ├── entitys │ │ ├── ChatAIState.dart │ │ └── llmMessage.dart │ └── image_packer.dart │ ├── action_and_intents.dart │ ├── constants.dart │ ├── events.dart │ ├── pages │ ├── regex │ │ └── edit_global_regex.dart │ ├── chat │ │ └── prompt_preview_page.dart │ └── character │ │ └── more_firstmessage_page.dart │ ├── widgets │ ├── AvatarImage.dart │ ├── webview │ │ ├── message_webview.dart │ │ └── chat_webview.dart │ ├── inner_app_bar.dart │ ├── custom_bottom_bar.dart │ ├── sizeAnimated.dart │ ├── stack_avatar.dart │ ├── chat │ │ ├── example_chat.dart │ │ ├── member_selector.dart │ │ ├── character_wheel.dart │ │ └── character_executer.dart │ ├── icon_switch_button.dart │ ├── toggleChip.dart │ └── BreadcrumbNavigation.dart │ ├── models │ ├── settings │ │ ├── prompt_setting_model.dart │ │ └── quick_command.dart │ ├── history_model.dart │ ├── lorebook_model.dart │ ├── prompt_model.dart │ └── chat_metadata_model.dart │ └── providers │ ├── log_controller.dart │ ├── web_session_controller.dart │ ├── prompt_controller.dart │ ├── lorebook_controller.dart │ └── chat_option_controller.dart ├── test └── widget_test.dart ├── linux ├── .gitignore ├── main.cc ├── flutter │ ├── generated_plugin_registrant.h │ ├── generated_plugin_registrant.cc │ ├── generated_plugins.cmake │ └── CMakeLists.txt └── my_application.h ├── ios ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist ├── RunnerTests │ └── RunnerTests.swift └── .gitignore ├── .vscode └── settings.json ├── macos ├── Flutter │ ├── Flutter-Debug.xcconfig │ ├── Flutter-Release.xcconfig │ └── GeneratedPluginRegistrant.swift ├── Runner │ ├── Configs │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ ├── Warnings.xcconfig │ │ └── AppInfo.xcconfig │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ ├── app_icon_128.png │ │ │ ├── app_icon_16.png │ │ │ ├── app_icon_256.png │ │ │ ├── app_icon_32.png │ │ │ ├── app_icon_512.png │ │ │ ├── app_icon_64.png │ │ │ ├── app_icon_1024.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Release.entitlements │ ├── DebugProfile.entitlements │ ├── MainFlutterWindow.swift │ └── Info.plist ├── .gitignore ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme └── RunnerTests │ └── RunnerTests.swift ├── web ├── favicon.png ├── icons │ ├── Icon-192.png │ ├── Icon-512.png │ ├── Icon-maskable-192.png │ └── Icon-maskable-512.png ├── manifest.json └── index.html ├── images ├── android.jpg ├── desktop.gif ├── desktop2.gif ├── mobile2.jpg ├── mobile3.jpg ├── mobile4.jpg ├── mobile5.jpg └── mobile6.jpg ├── howtobuildAndroid.txt ├── windows ├── runner │ ├── resources │ │ └── app_icon.ico │ ├── resource.h │ ├── runner.exe.manifest │ ├── utils.h │ ├── flutter_window.h │ ├── main.cpp │ ├── CMakeLists.txt │ ├── utils.cpp │ ├── flutter_window.cpp │ ├── Runner.rc │ └── win32_window.h ├── .gitignore └── flutter │ ├── generated_plugin_registrant.h │ ├── generated_plugin_registrant.cc │ └── generated_plugins.cmake ├── android ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── drawable │ │ │ │ │ ├── avatar.png │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── playstore-icon.png │ │ │ │ ├── drawable-v21 │ │ │ │ │ ├── avatar.png │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-ldpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── flutter_example │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── build.gradle └── settings.gradle ├── devtools_options.yaml ├── .gitignore ├── LICENSE.txt ├── analysis_options.yaml ├── .metadata ├── README.md └── assets └── webview └── relation_map └── index.html /lib/chat-app/test.dart: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmake.ignoreCMakeListsMissing": true 3 | } -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "ephemeral/Flutter-Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/web/favicon.png -------------------------------------------------------------------------------- /images/android.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/images/android.jpg -------------------------------------------------------------------------------- /images/desktop.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/images/desktop.gif -------------------------------------------------------------------------------- /images/desktop2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/images/desktop2.gif -------------------------------------------------------------------------------- /images/mobile2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/images/mobile2.jpg -------------------------------------------------------------------------------- /images/mobile3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/images/mobile3.jpg -------------------------------------------------------------------------------- /images/mobile4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/images/mobile4.jpg -------------------------------------------------------------------------------- /images/mobile5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/images/mobile5.jpg -------------------------------------------------------------------------------- /images/mobile6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/images/mobile6.jpg -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "ephemeral/Flutter-Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/web/icons/Icon-512.png -------------------------------------------------------------------------------- /howtobuildAndroid.txt: -------------------------------------------------------------------------------- 1 | // 指定一个架构以防止逆天包体积 2 | 3 | flutter build apk --release --target-platform android-arm64 -------------------------------------------------------------------------------- /web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /lib/chat-app/utils/PackageValue.dart: -------------------------------------------------------------------------------- 1 | class PackageValue { 2 | final T value; 3 | const PackageValue(this.value); 4 | } 5 | -------------------------------------------------------------------------------- /macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/android/app/src/main/res/drawable/avatar.png -------------------------------------------------------------------------------- /android/app/src/main/res/playstore-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/android/app/src/main/res/playstore-icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/android/app/src/main/res/drawable-v21/avatar.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-ldpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/android/app/src/main/res/mipmap-ldpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilimited/sillyChat/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/flutter_example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.bilimited.silly_chat 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | org.gradle.java.home=E:\\apps\\Android Studio\\jbr -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /devtools_options.yaml: -------------------------------------------------------------------------------- 1 | description: This file stores settings for Dart & Flutter DevTools. 2 | documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states 3 | extensions: 4 | - provider: false -------------------------------------------------------------------------------- /macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @main 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 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 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/chat-app/utils/FileUtils.dart: -------------------------------------------------------------------------------- 1 | import 'package:path/path.dart' as p; 2 | 3 | class Fileutils { 4 | static bool isChatFile(String path) { 5 | return p.extension(path) == '.chat'; 6 | } 7 | 8 | static bool comparePath(String path1, String path2) { 9 | return path1.replaceAll('/', '\\') == path2.replaceAll('/', '\\'); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /macos/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.server 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/chat-app/utils/chat/goto_chat.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_example/chat-app/providers/chat_controller.dart'; 2 | import 'package:flutter_example/chat-app/providers/chat_session_controller.dart'; 3 | 4 | class GotoChat { 5 | 6 | static void byPath(String path){ 7 | if(path.isEmpty){ 8 | return; 9 | } 10 | ChatController.of.currentChat.value = 11 | ChatSessionController(path); 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/chat-app/utils/chat/token_calc.dart: -------------------------------------------------------------------------------- 1 | class TokenCalc { 2 | static int estimateTokens(String text) { 3 | int tokenCount = 0; 4 | // 简单的遍历,性能极高,可在 UI 线程直接跑 5 | for (int i = 0; i < text.length; i++) { 6 | // 判断是否为汉字 (简单判断范围) 7 | if (text.codeUnitAt(i) > 255) { 8 | tokenCount += 2; // 汉字粗略按2算(有的模型接近1) 9 | } else { 10 | tokenCount += 1; // 英文标点粗略按1算(实际上英文是0.25左右,这里需要根据平均词长调整逻辑) 11 | } 12 | } 13 | return (tokenCount * 0.6).round(); // 根据实际模型微调系数 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/chat-app/action_and_intents.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_example/chat-app/pages/log_page.dart'; 4 | import 'package:flutter_example/chat-app/utils/customNav.dart'; 5 | 6 | class GotoLogPageIntent extends Intent { 7 | const GotoLogPageIntent(); 8 | } 9 | 10 | class GotoLogPageAction extends Action { 11 | final BuildContext context; 12 | GotoLogPageAction(this.context); 13 | 14 | @override 15 | void invoke(GotoLogPageIntent intent) { 16 | customNavigate(LogPage(), context: context); 17 | } 18 | } -------------------------------------------------------------------------------- /lib/chat-app/constants.dart: -------------------------------------------------------------------------------- 1 | import 'package:flex_color_scheme/flex_color_scheme.dart'; 2 | 3 | class Constants { 4 | static const CHANGE_LOG = """ 5 | **警告:检测到你正从旧版本升级到1.17.x。SillyChat-1.17.0进行了一些破坏性更新。** 6 | 7 | 如果你从酒馆导入了预设,或自己创建了预设,这些预设可能会失效,请重新导入它们。 8 | 9 | 此外,旧版本中的自动标题设置和摘要生成设置也会被重置。 10 | """; 11 | static const SHOW_CHANGE_LOG = false; 12 | 13 | // TODO:替换所有的硬编码chat 14 | static const CHAT_FOLDER_NAME = 'chat'; 15 | 16 | static const DEFAULT_THEME_NAME = "greyLaw"; 17 | static const DEFAULT_THEME = FlexScheme.greyLaw; 18 | 19 | static const USER_ID = 0; 20 | } 21 | -------------------------------------------------------------------------------- /lib/chat-app/events.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_example/chat-app/models/chat_model.dart'; 2 | import 'package:flutter_example/chat-app/models/message_model.dart'; 3 | 4 | abstract class AppEvent {} 5 | 6 | class FileDeletedEvent extends AppEvent { 7 | final String filePath; 8 | 9 | FileDeletedEvent(this.filePath); 10 | } 11 | 12 | class FileCreatedEvent extends AppEvent { 13 | final String filePath; 14 | FileCreatedEvent(this.filePath); 15 | } 16 | 17 | class NewMessageEvent extends AppEvent { 18 | final MessageModel message; 19 | final ChatModel chat; 20 | 21 | NewMessageEvent(this.message, this.chat); 22 | } 23 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = flutter_example 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterExample 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. 15 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /lib/chat-app/utils/json_util.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | class JsonUtil { 4 | static String? encode(dynamic data) { 5 | try { 6 | return const JsonEncoder.withIndent(' ').convert(data); 7 | } catch (_) { 8 | return null; 9 | } 10 | } 11 | 12 | static String format(String json) { 13 | if (json.trim().isEmpty) return json; 14 | try { 15 | final dynamic data = jsonDecode(json); 16 | return const JsonEncoder.withIndent(' ').convert(data); 17 | } catch (_) { 18 | return json; 19 | } 20 | } 21 | 22 | static String formatMap(dynamic data) { 23 | try { 24 | return const JsonEncoder.withIndent(' ').convert(data); 25 | } catch (_) { 26 | return data; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | // google() 4 | // mavenCentral() 5 | maven { url = uri("https://maven.aliyun.com/repository/releases") } 6 | maven { url = uri("https://maven.aliyun.com/repository/google") } 7 | maven { url = uri("https://maven.aliyun.com/repository/central") } 8 | maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") } 9 | maven { url = uri("https://maven.aliyun.com/repository/public") } 10 | } 11 | } 12 | 13 | rootProject.buildDir = "../build" 14 | subprojects { 15 | project.buildDir = "${rootProject.buildDir}/${project.name}" 16 | } 17 | subprojects { 18 | project.evaluationDependsOn(":app") 19 | } 20 | 21 | tasks.register("clean", Delete) { 22 | delete rootProject.buildDir 23 | } 24 | -------------------------------------------------------------------------------- /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 | 12 | void fl_register_plugins(FlPluginRegistry* registry) { 13 | g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = 14 | fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); 15 | file_selector_plugin_register_with_registrar(file_selector_linux_registrar); 16 | g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = 17 | fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); 18 | url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); 19 | } 20 | -------------------------------------------------------------------------------- /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 | 13 | void RegisterPlugins(flutter::PluginRegistry* registry) { 14 | FileSelectorWindowsRegisterWithRegistrar( 15 | registry->GetRegistrarForPlugin("FileSelectorWindows")); 16 | FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar( 17 | registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi")); 18 | UrlLauncherWindowsRegisterWithRegistrar( 19 | registry->GetRegistrarForPlugin("UrlLauncherWindows")); 20 | } 21 | -------------------------------------------------------------------------------- /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.6.0" apply false 22 | id "org.jetbrains.kotlin.android" version "2.2.0" apply false 23 | } 24 | 25 | include ":app" 26 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | file_selector_linux 7 | url_launcher_linux 8 | ) 9 | 10 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 11 | ) 12 | 13 | set(PLUGIN_BUNDLED_LIBRARIES) 14 | 15 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 16 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 17 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 18 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 19 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 20 | endforeach(plugin) 21 | 22 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 23 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 24 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 25 | endforeach(ffi_plugin) 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | .VSCodeCounter/ 13 | 14 | # IntelliJ related 15 | *.iml 16 | *.ipr 17 | *.iws 18 | .idea/ 19 | 20 | # The .vscode folder contains launch configuration and tasks you configure in 21 | # VS Code which you may wish to be included in version control, so this line 22 | # is commented out by default. 23 | #.vscode/ 24 | 25 | # Flutter/Dart/Pub related 26 | **/doc/api/ 27 | **/ios/Flutter/.last_build_id 28 | .dart_tool/ 29 | .flutter-plugins 30 | .flutter-plugins-dependencies 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | android/gradle.properties 46 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | file_selector_windows 7 | flutter_inappwebview_windows 8 | url_launcher_windows 9 | ) 10 | 11 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 12 | ) 13 | 14 | set(PLUGIN_BUNDLED_LIBRARIES) 15 | 16 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 17 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 18 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 19 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 20 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 21 | endforeach(plugin) 22 | 23 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 24 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 25 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 26 | endforeach(ffi_plugin) 27 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flutter_example", 3 | "short_name": "flutter_example", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Bilimited 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import file_picker 9 | import file_selector_macos 10 | import flutter_image_compress_macos 11 | import flutter_inappwebview_macos 12 | import package_info_plus 13 | import path_provider_foundation 14 | import url_launcher_macos 15 | 16 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 17 | FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) 18 | FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) 19 | FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin")) 20 | InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) 21 | FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) 22 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 23 | UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 24 | } 25 | -------------------------------------------------------------------------------- /lib/chat-app/utils/lorebooks/memory_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_example/chat-app/models/lorebook_item_model.dart'; 2 | import 'package:flutter_example/chat-app/models/lorebook_model.dart'; 3 | import 'package:flutter_example/chat-app/providers/character_controller.dart'; 4 | import 'package:flutter_example/chat-app/providers/lorebook_controller.dart'; 5 | 6 | class MemoryUtils { 7 | static void tryAddMemoryToCharacter(int charId, String summary) { 8 | final char = CharacterController.of.getCharacterById(charId); 9 | final mem = char.memoryBook; 10 | if (mem != null) { 11 | addMemory(mem, summary); 12 | } 13 | } 14 | 15 | static void addMemory(LorebookModel lorebook, String summary) { 16 | LorebookItemModel item = LorebookItemModel( 17 | id: DateTime.now().microsecondsSinceEpoch, 18 | name: "记忆-${DateTime.now().toString()}", 19 | content: summary) 20 | .copyWith( 21 | activationType: ActivationType.always, 22 | position: "memory", 23 | ); 24 | 25 | lorebook.items.add(item); 26 | 27 | LoreBookController.of.updateLorebook(lorebook); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /lib/chat-app/utils/sillyTavern/STRegexImporter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_example/chat-app/models/regex_model.dart'; 2 | 3 | abstract class STRegexImporter { 4 | static RegexModel? fromJson(Map json, String fileName, 5 | {int? id}) { 6 | try { 7 | RegexModel regexModel = RegexModel( 8 | id: id ?? DateTime.now().microsecondsSinceEpoch, 9 | name: json['scriptName'] ?? fileName, 10 | pattern: json['findRegex'], 11 | replacement: json['replaceString']); 12 | 13 | regexModel.enabled = !(json['disabled'] ?? true); 14 | 15 | if (json['promptOnly'] == true) { 16 | regexModel.onRequest = true; 17 | } else { 18 | regexModel.onAddMessage = true; 19 | } 20 | 21 | regexModel.depthMin = json['minDepth'] ?? 0; 22 | regexModel.depthMax = json['maxDepth'] ?? -1; 23 | 24 | List placement = json['placement']; 25 | if (placement.contains(1)) { 26 | regexModel.scopeUser = true; 27 | } 28 | if (placement.contains(2)) { 29 | regexModel.scopeAssistant = true; 30 | } 31 | 32 | List trim = json['trimStrings']; 33 | regexModel.trim = trim.join('\n'); 34 | 35 | return regexModel; 36 | } catch (e) { 37 | rethrow; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/chat-app/utils/markdown/latex_block_syntax.dart: -------------------------------------------------------------------------------- 1 | import 'package:markdown/markdown.dart'; 2 | 3 | class LatexBlockSyntax extends BlockSyntax { 4 | @override 5 | RegExp get pattern => RegExp( 6 | r'^(?:(\${1,2})(?:\n|$))|(?:(?:\\\[(.+)\\\])(?:\n|$))', 7 | multiLine: true, 8 | ); 9 | 10 | LatexBlockSyntax() : super(); 11 | 12 | @override 13 | List parseChildLines(BlockParser parser) { 14 | final m = pattern.firstMatch(parser.current.content); 15 | if (m?[2] != null) { 16 | parser.advance(); 17 | return [Line(m?[2] ?? '')]; 18 | } 19 | 20 | final childLines = []; 21 | parser.advance(); 22 | 23 | while (!parser.isDone) { 24 | final match = pattern.hasMatch(parser.current.content); 25 | if (!match) { 26 | childLines.add(parser.current); 27 | parser.advance(); 28 | } else { 29 | parser.advance(); 30 | break; 31 | } 32 | } 33 | 34 | return childLines; 35 | } 36 | 37 | @override 38 | Node parse(BlockParser parser) { 39 | final lines = parseChildLines(parser); 40 | final content = lines.map((e) => e.content).join('\n').trim(); 41 | final textElement = Element.text('latex', content); 42 | textElement.attributes['MathStyle'] = 'display'; 43 | 44 | return Element('p', [textElement]); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/chat-app/utils/service_handlers/ServiceHandler.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_example/chat-app/models/api_model.dart'; 2 | import 'package:flutter_example/chat-app/utils/AIHandler.dart'; 3 | import 'package:flutter_example/chat-app/utils/entitys/RequestOptions.dart'; 4 | import 'package:flutter_example/chat-app/utils/entitys/llmMessage.dart'; 5 | 6 | abstract class Servicehandler { 7 | final String baseUrl; 8 | final String name; 9 | final List defaultModelList; 10 | 11 | const Servicehandler({ 12 | required this.baseUrl, 13 | required this.name, 14 | required this.defaultModelList, 15 | }); 16 | 17 | bool get canFetchBalance => false; 18 | 19 | // TODO:模型自定义API选项 20 | 21 | // 获取模型列表 22 | Future> fetchModelList(String apikey); 23 | 24 | // 测试连通性 25 | Future testConnectivity(); 26 | 27 | // 发送API请求,同时包含了结果处理 28 | Stream request( 29 | Aihandler aihandler, LLMRequestOptions options, ApiModel api); 30 | 31 | // 将中间消息格式转换为服务商专用数据格式 32 | Future parseMessage(LLMMessage message); 33 | 34 | Future> getRequestBody(LLMRequestOptions options); 35 | 36 | // 消息预处理(在合并消息之前执行) 37 | List processMessage(List messages) { 38 | return messages; 39 | } 40 | 41 | Future fetchBalance(String apiKey) async { 42 | return "查询余额方法未实现!"; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | flutter_example 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /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"flutter_example", 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 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | 11 | linter: 12 | # The lint rules applied to this project can be customized in the 13 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 14 | # included above or to enable additional rules. A list of all available lints 15 | # and their documentation is published at https://dart.dev/lints. 16 | # 17 | # Instead of disabling a lint rule for the entire project in the 18 | # section below, it can also be suppressed for a single line of code 19 | # or a specific dart file by using the `// ignore: name_of_lint` and 20 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 21 | # producing the lint. 22 | rules: 23 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 24 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 25 | 26 | # Additional information about this file can be found at 27 | # https://dart.dev/guides/language/analysis-options 28 | -------------------------------------------------------------------------------- /lib/chat-app/pages/regex/edit_global_regex.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_example/chat-app/providers/vault_setting_controller.dart'; 3 | import 'package:flutter_example/chat-app/widgets/other/regex_list_editor.dart'; 4 | import 'package:get/get.dart'; 5 | 6 | class EditGlobalRegexPage extends StatefulWidget { 7 | EditGlobalRegexPage({super.key}); 8 | 9 | @override 10 | State createState() { 11 | return _EditGlobalRegexPageState(); 12 | } 13 | } 14 | 15 | class _EditGlobalRegexPageState extends State { 16 | VaultSettingController get settingController => 17 | Get.find(); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return Scaffold( 22 | appBar: AppBar( 23 | title: Text('编辑全局正则'), 24 | ), 25 | body: Center( 26 | child: 27 | Column( 28 | children: [ 29 | Obx(() => Expanded(child: RegexListEditor( 30 | // 傻逼GetX 31 | // ignore: invalid_use_of_protected_member 32 | regexList: settingController.regexes.value, 33 | onChanged: (reg) { 34 | settingController.regexes.value = reg; 35 | settingController.saveSettings(); 36 | }, 37 | )), 38 | 39 | ) , 40 | SizedBox(height: 32,) 41 | ], 42 | )), 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/chat-app/widgets/AvatarImage.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_example/chat-app/providers/setting_controller.dart'; 4 | import 'package:path/path.dart' as p; 5 | 6 | /// 一个用于显示头像图片的组件,支持异步加载和占位符。 7 | /// 头像的形状和边框由其父组件提供。 8 | class AvatarImage extends StatelessWidget { 9 | /// 图片的文件名(不包含路径和扩展名)。 10 | final String fileName; 11 | 12 | final double? width; 13 | final double? height; 14 | 15 | const AvatarImage({ 16 | Key? key, 17 | required this.fileName, 18 | this.width, 19 | this.height, 20 | }) : super(key: key); 21 | 22 | /// 异步获取图片文件的完整路径。 23 | static String getPath(String filename) { 24 | return '${SettingController.of.getImagePathSync()}/${p.basename(filename)}'; 25 | } 26 | 27 | static Widget round(String path, double radius) { 28 | return ClipRRect( 29 | borderRadius: BorderRadiusGeometry.circular(114514), 30 | child: 31 | AvatarImage(fileName: path, width: radius * 2, height: radius * 2)); 32 | } 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | final file = File(getPath(fileName)); 37 | return Image.file( 38 | file, 39 | width: width, 40 | height: height, 41 | fit: BoxFit.cover, 42 | errorBuilder: (context, error, stackTrace) { 43 | return Icon( 44 | Icons.account_circle, 45 | color: Theme.of(context).colorScheme.secondary, 46 | size: width, 47 | ); 48 | }, 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. 5 | id "dev.flutter.flutter-gradle-plugin" 6 | } 7 | 8 | android { 9 | namespace = "com.bilimited.silly_chat" 10 | compileSdk = 36 11 | ndkVersion = "29.0.13113456" 12 | 13 | compileOptions { 14 | sourceCompatibility = JavaVersion.VERSION_17 15 | targetCompatibility = JavaVersion.VERSION_17 16 | } 17 | 18 | kotlinOptions { 19 | jvmTarget = JavaVersion.VERSION_17 20 | } 21 | 22 | defaultConfig { 23 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 24 | applicationId = "com.bilimited.silly_chat" 25 | // You can update the following values to match your application needs. 26 | // For more information, see: https://flutter.dev/to/review-gradle-config. 27 | minSdk = flutter.minSdkVersion 28 | targetSdk = flutter.targetSdkVersion 29 | versionCode = flutter.versionCode 30 | versionName = flutter.versionName 31 | } 32 | 33 | buildTypes { 34 | release { 35 | // TODO: Add your own signing config for the release build. 36 | // Signing with the debug keys for now, so `flutter run --release` works. 37 | signingConfig = signingConfigs.debug 38 | } 39 | } 40 | } 41 | 42 | flutter { 43 | source = "../.." 44 | } 45 | -------------------------------------------------------------------------------- /lib/chat-app/utils/customNav.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_example/main.dart'; 3 | 4 | /// 自定义跳转函数 5 | /// [page] 目标页面 6 | /// [context] 移动端有些页面无法跳转,需要用另一种方法跳转 7 | /// [rootNav] 仅移动端有效:顶级页面跳转。若设为false,则会在调用该方法的页面的那一侧屏幕进行跳转 8 | /// 返回Future,可await获取返回值 9 | /// WARNING:在手机端若不传context参数可能导致无法跳转页面 10 | Future customNavigate(Widget page, 11 | {required BuildContext context, bool rootNav = true}) async { 12 | if (SillyChatApp.isDesktop()) { 13 | // 桌面端:用Dialog包裹页面 14 | return await showDialog( 15 | context: context, 16 | barrierDismissible: true, 17 | builder: (context) => Dialog( 18 | backgroundColor: Theme.of(context).colorScheme.surface, 19 | shape: RoundedRectangleBorder( 20 | borderRadius: BorderRadius.circular(12), 21 | ), 22 | elevation: 16, // 提高阴影 23 | shadowColor: Colors.black.withOpacity(0.3), // 自定义阴影颜色 24 | insetPadding: EdgeInsets.symmetric(horizontal: 100, vertical: 50), 25 | child: Padding( 26 | padding: const EdgeInsets.all(12.0), // 防止内容与圆角重叠 27 | child: ConstrainedBox( 28 | constraints: BoxConstraints( 29 | maxWidth: 600, 30 | maxHeight: 800, 31 | ), 32 | child: page, 33 | ), 34 | ), 35 | ), 36 | ); 37 | } else { 38 | // 移动端:直接跳转 39 | return await Navigator.of(context, rootNavigator: rootNav).push( 40 | MaterialPageRoute(builder: (_) => page), 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/chat-app/utils/entitys/ChatAIState.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_example/chat-app/models/character_model.dart'; 2 | import 'package:flutter_example/chat-app/utils/AIHandler.dart'; 3 | 4 | /** TODO:本类意义不明,回头改一下 */ 5 | class ChatAIState { 6 | final String id; 7 | final String LLMBuffer; 8 | final String GenerateState; 9 | final bool isGenerating; 10 | final MessageStyle style; 11 | final int currentAssistant; 12 | final Aihandler aihandler; 13 | 14 | ChatAIState({ 15 | this.id = "_", 16 | this.LLMBuffer = "", 17 | this.GenerateState = "", 18 | this.style = MessageStyle.common, 19 | this.isGenerating = false, 20 | this.currentAssistant = -1, 21 | required this.aihandler, 22 | }); 23 | 24 | ChatAIState copyWith({ 25 | String? LLMBuffer, 26 | String? GenerateState, 27 | bool? isGenerating, 28 | int? currentAssistant, 29 | MessageStyle? style, 30 | }) { 31 | return ChatAIState( 32 | LLMBuffer: LLMBuffer ?? this.LLMBuffer, 33 | GenerateState: GenerateState ?? this.GenerateState, 34 | isGenerating: isGenerating ?? this.isGenerating, 35 | currentAssistant: currentAssistant ?? this.currentAssistant, 36 | aihandler: this.aihandler, 37 | style: style ?? this.style); 38 | } 39 | 40 | toJson() { 41 | return { 42 | "id": id, 43 | "LLMBuffer": LLMBuffer, 44 | "GenerateState": GenerateState, 45 | "isGenerating": isGenerating, 46 | "style": style.index, 47 | "currentAssistant": currentAssistant, 48 | }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/chat-app/utils/markdown/latex_element_builder.dart: -------------------------------------------------------------------------------- 1 | // import 'package:flutter/material.dart'; 2 | // import 'package:flutter_markdown/flutter_markdown.dart'; 3 | // import 'package:flutter_math_fork/flutter_math.dart'; 4 | // import 'package:markdown/markdown.dart' as md; 5 | 6 | // class LatexElementBuilder extends MarkdownElementBuilder { 7 | // LatexElementBuilder({ 8 | // this.textStyle, 9 | // this.textScaleFactor, 10 | // }); 11 | 12 | // /// The style to apply to the text. 13 | // final TextStyle? textStyle; 14 | 15 | // /// The text scale factor to apply to the text. 16 | // final double? textScaleFactor; 17 | 18 | // @override 19 | // Widget visitElementAfterWithContext( 20 | // BuildContext context, 21 | // md.Element element, 22 | // TextStyle? preferredStyle, 23 | // TextStyle? parentStyle, 24 | // ) { 25 | // final String text = element.textContent; 26 | // if (text.isEmpty) { 27 | // return const SizedBox(); 28 | // } 29 | 30 | // MathStyle mathStyle; 31 | // switch (element.attributes['MathStyle']) { 32 | // case 'text': 33 | // mathStyle = MathStyle.text; 34 | // case 'display': 35 | // mathStyle = MathStyle.display; 36 | // default: 37 | // mathStyle = MathStyle.text; 38 | // } 39 | 40 | // return SingleChildScrollView( 41 | // scrollDirection: Axis.horizontal, 42 | // clipBehavior: Clip.antiAlias, 43 | // child: Math.tex( 44 | // text, 45 | // textStyle: textStyle, 46 | // mathStyle: mathStyle, 47 | // textScaleFactor: textScaleFactor, 48 | // ), 49 | // ); 50 | // } 51 | // } 52 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/chat-app/models/settings/prompt_setting_model.dart: -------------------------------------------------------------------------------- 1 | // 各种预设提示词/对话 2 | class PromptSettingModel { 3 | // 用于让AI连续输出 4 | String continuePrompt = "继续"; 5 | 6 | // 在连续Assistant消息之间添加的用户消息分隔符 7 | String interAssistantUserSeparator = "继续"; 8 | 9 | // 群聊中添加在每条消息开头 10 | String groupFormatter = ":"; 11 | 12 | // 是否格式化正文 13 | bool isFormatMainContent = false; 14 | 15 | PromptSettingModel(); 16 | 17 | // JSON序列化 18 | Map toJson() { 19 | return { 20 | 'continuePrompt': continuePrompt, 21 | 'interAssistantUserSeparator': interAssistantUserSeparator, 22 | 'groupFormatter': groupFormatter, 23 | 'isFormatMainContent': isFormatMainContent, 24 | }; 25 | } 26 | 27 | // 从JSON反序列化 28 | factory PromptSettingModel.fromJson(Map json) { 29 | return PromptSettingModel() 30 | ..continuePrompt = json['continuePrompt'] ?? "继续" 31 | ..interAssistantUserSeparator = 32 | json['interAssistantUserSeparator'] ?? "继续" 33 | ..isFormatMainContent = json['isFormatMainContent'] ?? false 34 | ..groupFormatter = json['groupFormatter'] ?? ":"; 35 | } 36 | 37 | copyWith({ 38 | String? continuePrompt, 39 | String? interAssistantUserSeparator, 40 | String? groupFormatter, 41 | bool? isFormatMainContent, 42 | }) { 43 | return PromptSettingModel() 44 | ..continuePrompt = continuePrompt ?? this.continuePrompt 45 | ..interAssistantUserSeparator = 46 | interAssistantUserSeparator ?? this.interAssistantUserSeparator 47 | ..groupFormatter = groupFormatter ?? this.groupFormatter 48 | ..isFormatMainContent = isFormatMainContent ?? this.isFormatMainContent; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/chat-app/widgets/webview/message_webview.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_example/chat-app/providers/web_session_controller.dart'; 3 | import 'package:flutter_inappwebview/flutter_inappwebview.dart'; 4 | import 'package:get/get.dart'; 5 | 6 | class MessageWebview extends StatefulWidget { 7 | const MessageWebview({ 8 | super.key, 9 | required this.content, 10 | }); 11 | final String content; 12 | 13 | @override 14 | State createState() => _MessageWebviewState(); 15 | } 16 | 17 | class _MessageWebviewState extends State { 18 | late InAppWebViewController _webViewController; 19 | 20 | // late WebSessionController _sessionController = 21 | // Get.put(WebSessionController(webViewController: _webViewController)); 22 | 23 | String get _htmlContent { 24 | final content = widget.content.trim(); 25 | final regex = 26 | RegExp(r'```html\s*([\s\S]*?)```|```([\s\S]*?)```', multiLine: true); 27 | final match = regex.firstMatch(content); 28 | if (match != null) { 29 | // Prefer group 1 (```html ... ```) if present, else group 2 (```...```) 30 | return (match.group(1) ?? match.group(2))?.trim() ?? content; 31 | } 32 | return content; 33 | } 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | return Scaffold( 38 | appBar: AppBar(), 39 | body: InAppWebView( 40 | initialData: InAppWebViewInitialData(data: _htmlContent), 41 | onWebViewCreated: (controller) { 42 | _webViewController = controller; 43 | 44 | //_sessionController.onWebViewCreated(controller); 45 | }, 46 | onConsoleMessage: (controller, consoleMessage) { 47 | print(consoleMessage); 48 | }, 49 | //initialUserScripts: , 50 | ), 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Flutter Example 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | flutter_example 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | CADisableMinimumFrameDurationOnPhone 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /.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: android 19 | create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 20 | base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 21 | - platform: ios 22 | create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 23 | base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 24 | - platform: linux 25 | create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 26 | base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 27 | - platform: macos 28 | create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 29 | base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 30 | - platform: web 31 | create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 32 | base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 33 | - platform: windows 34 | create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 35 | base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /lib/chat-app/widgets/inner_app_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class InnerAppBar extends StatelessWidget implements PreferredSizeWidget { 4 | final Widget? title; 5 | final List? actions; 6 | final Widget? leading; 7 | 8 | static const double height = 48.0; // 默认是 56.0,这里设为 40 9 | static const double iconSize = 20.0; // 默认是 24.0 10 | static const double titleSize = 16.0; // 默认是 20.0 11 | 12 | const InnerAppBar({ 13 | Key? key, 14 | this.title, 15 | this.actions, 16 | this.leading, 17 | }) : super(key: key); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | final theme = Theme.of(context); 22 | return AppBar( 23 | titleTextStyle: TextStyle( 24 | fontSize: titleSize, 25 | fontWeight: FontWeight.bold, 26 | color: theme.colorScheme.onSurface 27 | // color: Colors.black, // 需根据背景调整颜色 28 | ), 29 | toolbarHeight: height, 30 | iconTheme: const IconThemeData( 31 | size: iconSize, 32 | // color: Colors.black, 33 | ), 34 | actionsIconTheme: const IconThemeData( 35 | size: iconSize, 36 | // color: Colors.black, 37 | ), 38 | 39 | // 3. 调整左侧区域宽度 40 | // 默认是 56,如果不改小,左上角图标旁边会有大片空白 41 | leadingWidth: height, 42 | backgroundColor: Colors.transparent, // 核心样式 43 | surfaceTintColor: Colors.transparent, 44 | shadowColor: Colors.transparent, 45 | foregroundColor: Colors.transparent, 46 | elevation: 0, // 去除阴影 47 | title: title, 48 | actions: actions, 49 | leading: leading, 50 | // 可以在这里统一处理状态栏颜色(黑/白) 51 | // systemOverlayStyle: SystemUiOverlayStyle.dark, 52 | 53 | // 这里可以添加更多统一的逻辑,比如统一的返回按钮图标等 54 | ); 55 | } 56 | 57 | @override 58 | // 必须实现此方法,指定AppBar的高度,通常是 kToolbarHeight (56.0) 59 | Size get preferredSize => const Size.fromHeight(height); 60 | } 61 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/chat-app/utils/entitys/llmMessage.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:flutter_example/chat-app/models/message_model.dart'; 5 | import 'package:flutter_example/chat-app/models/prompt_model.dart'; 6 | 7 | class LLMMessage { 8 | final String content; 9 | final String role; // 如 "user"、"assistant"、"system" 10 | final List fileDirs; // 上传文件目录列表 11 | final bool isPrompt; // prompt消息比常规消息优先级更高 12 | 13 | final int? senderId; // 这个没啥用 14 | 15 | LLMMessage({ 16 | required this.content, 17 | required this.role, 18 | this.fileDirs = const [], 19 | this.isPrompt = false, 20 | this.senderId, 21 | }); 22 | 23 | LLMMessage.fromJson(Map json) 24 | : content = json['content'] ?? '', 25 | role = json['role'] ?? 'user', 26 | fileDirs = (json['fileDirs'] as List? ?? []) 27 | .map((e) => e.toString()) 28 | .toList(), 29 | isPrompt = json['isPrompt'] ?? false, 30 | senderId = json['senderId']; 31 | 32 | /// 从 MessageModel 创建 33 | factory LLMMessage.fromMessageModel(MessageModel msg) { 34 | return LLMMessage( 35 | content: msg.content, 36 | role: msg.role.toString().split('.').last, 37 | fileDirs: msg.resPath, 38 | senderId: msg.senderId); 39 | } 40 | 41 | /// 从 PromptModel 创建 42 | factory LLMMessage.fromPromptModel(PromptModel prompt) { 43 | return LLMMessage( 44 | content: prompt.content, 45 | role: prompt.role, 46 | fileDirs: [], 47 | ); 48 | } 49 | 50 | LLMMessage copyWith({ 51 | String? content, 52 | String? role, 53 | List? fileDirs, 54 | bool? isPrompt, 55 | int? senderId, 56 | }) { 57 | return LLMMessage( 58 | content: content ?? this.content, 59 | role: role ?? this.role, 60 | fileDirs: fileDirs ?? this.fileDirs, 61 | isPrompt: isPrompt ?? this.isPrompt, 62 | senderId: senderId ?? this.senderId, 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/chat-app/providers/log_controller.dart: -------------------------------------------------------------------------------- 1 | // log_controller.dart 2 | import 'package:get/get.dart'; 3 | 4 | enum LogLevel { 5 | info, 6 | warning, 7 | error, 8 | } 9 | 10 | enum LogType { 11 | text, 12 | json // 假设未来可能需要展示JSON类型 13 | } 14 | 15 | class LogEntry { 16 | final String message; 17 | final LogLevel level; 18 | final DateTime timestamp; 19 | final LogType type; // 新增:日志类型 20 | final String? title; // 新增:日志标题 21 | 22 | LogEntry( 23 | {required this.message, 24 | required this.level, 25 | required this.timestamp, 26 | this.type = LogType.text, // 默认为文本类型 27 | this.title // 标题可以为空 28 | }); 29 | } 30 | 31 | class LogController extends GetxController { 32 | static LogController get to => Get.find(); 33 | 34 | final RxList _logs = [].obs; 35 | final RxInt _unread = 0.obs; 36 | static const int _maxLogs = 30; 37 | 38 | List get logs => _logs.toList(); 39 | int get unread => _unread.value; 40 | 41 | // 修改addLog方法,支持type和title 42 | LogEntry addLog(String message, LogLevel level, 43 | {LogType type = LogType.text, String? title}) { 44 | final entry = LogEntry( 45 | message: message, 46 | level: level, 47 | timestamp: DateTime.now(), 48 | type: type, 49 | title: title, 50 | ); 51 | _logs.insert(0, entry); 52 | 53 | if (_logs.length > _maxLogs) { 54 | _logs.removeLast(); 55 | } 56 | _unread.value++; 57 | 58 | return entry; 59 | } 60 | 61 | void clearLogs() { 62 | _logs.clear(); 63 | } 64 | 65 | void clearUnread() { 66 | _unread.value = 0; 67 | } 68 | 69 | List getLogsByLevel(LogLevel level) { 70 | return _logs.where((log) => log.level == level).toList(); 71 | } 72 | 73 | // 静态方法用于快速记录日志,支持title 74 | static LogEntry log(String message, LogLevel level, 75 | {LogType type = LogType.text, String? title}) { 76 | return LogController.to.addLog(message, level, title: title, type: type); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/chat-app/models/history_model.dart: -------------------------------------------------------------------------------- 1 | class HistoryModel { 2 | final List messageHistory; 3 | final List commandHistory; 4 | 5 | final List characterHistory; // 最近选择角色的历史 6 | 7 | // 最近打开聊天 8 | final List chatHistory; 9 | 10 | HistoryModel({ 11 | List? messageHistory, 12 | List? commandHistory, 13 | List? chatHistory, 14 | List? characterHistory, 15 | }) : messageHistory = messageHistory ?? [], 16 | commandHistory = commandHistory ?? [], 17 | chatHistory = chatHistory ?? [], 18 | characterHistory = characterHistory ?? []; 19 | 20 | factory HistoryModel.fromJson(Map json) { 21 | return HistoryModel( 22 | messageHistory: json['messageHistory'] != null 23 | ? List.from(json['messageHistory']) 24 | : [], 25 | commandHistory: json['commandHistory'] != null 26 | ? List.from(json['commandHistory']) 27 | : [], 28 | chatHistory: json['chatHistory'] != null 29 | ? List.from(json['chatHistory']) 30 | : [], 31 | characterHistory: json['characterHistory'] != null 32 | ? List.from(json['characterHistory']) 33 | : [], 34 | ); 35 | } 36 | 37 | Map toJson() { 38 | return { 39 | 'messageHistory': List.from(messageHistory), 40 | 'commandHistory': List.from(commandHistory), 41 | 'chatHistory': List.from(chatHistory), 42 | 'characterHistory': List.from(characterHistory), 43 | }; 44 | } 45 | 46 | HistoryModel copyWith({ 47 | List? messageHistory, 48 | List? commandHistory, 49 | List? chatHistory, 50 | List? characterHistory, 51 | }) { 52 | return HistoryModel( 53 | messageHistory: messageHistory ?? this.messageHistory, 54 | commandHistory: commandHistory ?? this.commandHistory, 55 | chatHistory: chatHistory ?? this.chatHistory, 56 | characterHistory: characterHistory ?? this.characterHistory, 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/chat-app/widgets/custom_bottom_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_example/chat-app/pages/other/api_manager.dart'; 3 | import 'package:flutter_example/chat-app/pages/settings/setting_page.dart'; 4 | import 'package:flutter_example/chat-app/pages/vault_manager.dart'; 5 | import 'package:flutter_example/chat-app/providers/setting_controller.dart'; 6 | import 'package:flutter_example/chat-app/utils/customNav.dart'; 7 | 8 | // 定义一个自定义的 Bottom Bar Widget 9 | class CustomBottomBar extends StatelessWidget { 10 | // 两个固定按钮的点击回调 11 | 12 | // 可被覆盖(定制)的主要按钮 Widget 13 | final Widget centerButton; 14 | 15 | // 构造函数 16 | const CustomBottomBar({ 17 | Key? key, 18 | required this.centerButton, // 要求传入中央按钮 19 | }) : super(key: key); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | // 使用 Stack 允许中央按钮可以覆盖在底部栏的上方或浮动 24 | return Row( 25 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 26 | children: [ 27 | // 左侧按钮 28 | Row( 29 | mainAxisAlignment: MainAxisAlignment.start, 30 | children: [ 31 | IconButton( 32 | icon: Icon(SettingController.of.isDarkMode.value 33 | ? Icons.dark_mode 34 | : Icons.light_mode), 35 | onPressed: () { 36 | SettingController.of.toggleDarkMode(); 37 | }, 38 | tooltip: '切换主题', 39 | ), 40 | IconButton( 41 | icon: const Icon(Icons.power), 42 | onPressed: () { 43 | customNavigate(ApiManagerPage(), context: context); 44 | }, 45 | tooltip: 'API', 46 | ), 47 | // 右侧按钮 (注意:在中心按钮位置留空,所以这里只放两个) 48 | IconButton( 49 | icon: const Icon(Icons.settings), 50 | onPressed: () { 51 | customNavigate(SettingPage(), context: context); 52 | }, 53 | tooltip: '设置', 54 | ), 55 | ], 56 | ), 57 | centerButton 58 | ], 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Silly Chat 2 | 3 | --- 4 | 5 | 没错,这又双叒叕是一个类酒馆前端项目。() 6 | 7 | 本项目为自用分享。这是一个模仿NextChat和SillyTavern的AI聊天软件,基于Flutter开发,支持桌面/移动端,性能优异(也许吧)。目前已经实现了角色、群聊、世界书、主题简单定制以及若干小功能。 8 | 9 | 项目定位大概是在NextChat和酒馆之间的APP,主打轻量级,灵活,所以不会有太多乱七八糟的东西(太复杂的功能,前端卡之类的,就算Flutter办得到我也做不出来orz),后续更新主要以基本功能完善/BUG修复/体验优化为主 10 | 11 | 无需部署,解压即用。 12 | 13 | > ⚠️本项目仍在开发中,不保证稳定性 14 | 15 | > ⚠️本项目开发以移动端为主,PC端可以使用,但部分操作逻辑未适配 16 | 17 | 桌面端预览 18 |
19 | 安卓端预览 20 | 安卓端预览 21 | 安卓端预览 22 |
23 | 24 | --- 25 | 26 | ## 主要功能 27 | 28 | - 📁 用文件夹管理聊天 29 | - 🍻 导入酒馆的对话预设、角色卡、世界书和正则 30 | - 🕸️ 角色关系网(待完善) 31 | - 👥 群聊功能(手动控制) 32 | - ✏️ 编辑消息内容/类型/发送者,批量复制粘贴消息记录 33 | - 🧩 实用功能:AI帮答、生成标题、导演模式、生成总结 34 | - 🔍 聊天记录搜索 35 | - 🔑 支持openAI、Gemini、DeepSeek和自定义API,支持余额查询 36 | - ☁️ webDav云同步数据 37 | - 🖼️ 发送图片,支持Gemini多模态 38 | - 🎨 支持自定义聊天窗口样式、界面主题等 39 | 40 | --- 41 | 42 | ## 操作指南 43 | 44 | 当前版本已经添加了引导界面,根据引导界面进行操作即可。如果在引导界面没有导入角色卡,则你需要手动创建一个角色才能开始聊天。 45 | 46 | --- 47 | 48 | ## 开发环境 49 | 50 | ``` 51 | Flutter 3.35.5 52 | 53 | // Android端开发环境: 54 | Android SDK 36 55 | Android NDK 29.0.13846066 56 | gradle 8.7 57 | 58 | // Windows端开发环境: 59 | Visual Studio 2022 60 | Nuget 61 | ``` 62 | 63 | --- 64 | 65 | ## FAQ 66 | 67 | **Q:是否支持自定义/本地模型?** 68 | 69 | A:支持兼容OpenAI的自定义模型。 70 | 71 | 72 | **Q:兼容酒馆吗?** 73 | 74 | A:允许导入酒馆的部分预设、正则、角色卡和世界书,暂不支持导出。目前对酒馆的兼容还处于试验阶段,可能会出现各种各样的问题。 75 | 76 | 77 | **Q:支持文生图/TTS吗?** 78 | 79 | A:暂不打算支持,如果你想添加这些功能,欢迎提交PR 80 | 81 | 82 | **Q:和酒馆相比有什么区别** 83 | 84 | A:操作逻辑不同。SillyChat一开始并不是为了兼容酒馆而设计,因此不少逻辑(尤其是提示词管理)和酒馆有较大的区别。 85 | 86 | 相对酒馆的优点:因为使用Flutter开发,支持跨平台,操作流畅;没有酒馆的历史包袱,添加新功能比较方便。缺点:也是因为用Flutter开发,很难像酒馆一样拥有灵活的插件、主题和前端卡。 87 | 88 | --- 89 | 90 | ## 贡献 91 | 虽然目前代码仍惨不忍睹,但欢迎提交PR! 92 | 你也可以提交Issue来汇报Bug或者一些有趣的功能建议_(:з」∠)_ 93 | 94 | --- 95 | 96 | ## 许可证 97 | 本项目采用 **MIT 许可证**。 98 | 99 | 对于电脑端,你可以点击主界面左下方省略号按钮->“查看第三方证书”来查看第三方许可证。 100 | -------------------------------------------------------------------------------- /lib/chat-app/utils/service_handlers/KimiServiceHandler.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart' show Dio, Options, DioException; 2 | import 'package:flutter_example/chat-app/utils/service_handlers/OpenAIServiceHandler.dart'; 3 | 4 | class Kimiservicehandler extends Openaiservicehandler { 5 | const Kimiservicehandler() 6 | : super( 7 | baseUrl: 'https://api.moonshot.cn/v1', 8 | name: 'kimi', 9 | defaultModelList: const []); 10 | 11 | @override 12 | bool get canFetchBalance => true; 13 | 14 | @override 15 | Future fetchBalance(String apiKey) async { 16 | final url = 'https://api.moonshot.cn/v1/users/me/balance'; 17 | final dio = Dio(); 18 | 19 | try { 20 | final response = await dio.get( 21 | url, 22 | options: Options( 23 | headers: { 24 | 'Authorization': 'Bearer $apiKey', // 使用 Bearer Token 认证 25 | }, 26 | ), 27 | ); 28 | 29 | if (response.statusCode == 200) { 30 | final data = response.data; 31 | 32 | // 检查响应状态 33 | final status = data['status'] as bool?; 34 | if (status != true) { 35 | return '❌ 接口返回失败: ${data['scode'] ?? '未知错误'}'; 36 | } 37 | 38 | final balanceData = data['data'] as Map?; 39 | 40 | if (balanceData == null) { 41 | return '❌ 数据解析失败: 未找到 data 字段'; 42 | } 43 | 44 | // 提取余额数据 45 | final availableBalance = balanceData['available_balance'] as num? ?? 0; 46 | final voucherBalance = balanceData['voucher_balance'] as num? ?? 0; 47 | final cashBalance = balanceData['cash_balance'] as num? ?? 0; 48 | 49 | // 构建 Markdown 50 | final markdown = ''' 51 | ### 🌙 Kimi (Moonshot AI) 账户余额 52 | 53 | 54 | - **总可用余额**: `$availableBalance` 55 | - **赠券余额**: `$voucherBalance` 56 | - **现金余额**: `$cashBalance` 57 | 58 | --- 59 | ✅ 状态: 成功 60 | '''; 61 | 62 | return markdown; 63 | } else { 64 | return '❌ 请求失败,状态码: ${response.statusCode}'; 65 | } 66 | } on DioException catch (e) { 67 | return '❌ 网络错误: ${e.message}'; 68 | } catch (e) { 69 | return '❌ 未知错误: $e'; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/chat-app/pages/chat/prompt_preview_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_example/chat-app/utils/entitys/llmMessage.dart'; 3 | 4 | class PromptPreviewPage extends StatefulWidget { 5 | final List messages; 6 | 7 | const PromptPreviewPage({Key? key, required this.messages}) : super(key: key); 8 | 9 | @override 10 | State createState() => _PromptPreviewPageState(); 11 | } 12 | 13 | class _PromptPreviewPageState extends State { 14 | bool isGroupMode = false; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return Scaffold( 19 | appBar: AppBar( 20 | title: Text('Prompt预览'), 21 | ), 22 | body: ListView.builder( 23 | itemCount: widget.messages.length, 24 | itemBuilder: (context, index) { 25 | final message = widget.messages[index]; 26 | return Card( 27 | margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), 28 | child: Column( 29 | crossAxisAlignment: CrossAxisAlignment.start, 30 | children: [ 31 | ListTile( 32 | title: Text( 33 | message.role, 34 | style: TextStyle( 35 | fontWeight: FontWeight.bold, 36 | color: _getRoleColor(message.role), 37 | ), 38 | ), 39 | subtitle: Text( 40 | message.content, 41 | ), 42 | ), 43 | Text( 44 | "长度: ${(message.content).length}", 45 | style: TextStyle(color: Colors.grey, fontSize: 12), 46 | ) 47 | ], 48 | )); 49 | }, 50 | ), 51 | ); 52 | } 53 | 54 | Color _getRoleColor(String? role) { 55 | switch (role) { 56 | case 'system': 57 | return Colors.purple; 58 | case 'assistant': 59 | return Colors.blue; 60 | case 'user': 61 | return Colors.green; 62 | default: 63 | return Colors.grey; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/chat-app/widgets/sizeAnimated.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SizeAnimatedWidget extends StatefulWidget { 4 | final Widget child; 5 | final bool visible; 6 | final Duration duration; 7 | final Curve curve; 8 | 9 | const SizeAnimatedWidget({ 10 | Key? key, 11 | required this.child, 12 | required this.visible, 13 | this.duration = const Duration(milliseconds: 350), 14 | this.curve = Curves.easeOutQuint, 15 | }) : super(key: key); 16 | 17 | @override 18 | State createState() => _SizeAnimatedWidgetState(); 19 | } 20 | 21 | class _SizeAnimatedWidgetState extends State 22 | with SingleTickerProviderStateMixin { 23 | late AnimationController _controller; 24 | late Animation _animation; 25 | bool _isVisible = false; 26 | 27 | @override 28 | void initState() { 29 | super.initState(); 30 | _controller = AnimationController( 31 | duration: widget.duration, 32 | vsync: this, 33 | ); 34 | _animation = CurvedAnimation( 35 | parent: _controller, 36 | curve: widget.curve, 37 | ); 38 | if (widget.visible) { 39 | _isVisible = true; 40 | _controller.forward(); 41 | } 42 | } 43 | 44 | @override 45 | void didUpdateWidget(SizeAnimatedWidget oldWidget) { 46 | super.didUpdateWidget(oldWidget); 47 | if (widget.visible != oldWidget.visible) { 48 | if (widget.visible) { 49 | setState(() => _isVisible = true); 50 | _controller.forward(); 51 | } else { 52 | _controller.reverse().then((value) { 53 | setState(() => _isVisible = false); 54 | }); 55 | } 56 | } 57 | } 58 | 59 | @override 60 | void dispose() { 61 | _controller.dispose(); 62 | super.dispose(); 63 | } 64 | 65 | @override 66 | Widget build(BuildContext context) { 67 | if (!_isVisible) return const SizedBox.shrink(); 68 | 69 | return AnimatedBuilder( 70 | animation: _animation, 71 | builder: (context, child) { 72 | return Transform.scale( 73 | scale: _animation.value, 74 | child: child, 75 | ); 76 | }, 77 | child: widget.child, 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/chat-app/models/settings/quick_command.dart: -------------------------------------------------------------------------------- 1 | 2 | // 快捷消息的发送者 3 | enum QuickCommandRole { 4 | narration, 5 | user, 6 | assistant, 7 | } 8 | 9 | class QuickCommand { 10 | 11 | final int id; 12 | final String name; 13 | final String description; 14 | 15 | // 指令的文字内容 16 | final String command; 17 | final QuickCommandRole role; 18 | 19 | // 发送快捷指令时是否将指令内容加入消息列表 20 | final bool addCommandToMessageList; 21 | 22 | // 发送快捷指令时绑定的聊天预设(null使用当前聊天的预设) 23 | final int? bindOption; 24 | 25 | QuickCommand({ 26 | required this.id, 27 | required this.name, 28 | required this.description, 29 | required this.command, 30 | this.role = QuickCommandRole.user, 31 | this.addCommandToMessageList = true, 32 | this.bindOption, 33 | }); 34 | 35 | // JSON序列化 36 | Map toJson() { 37 | return { 38 | 'id': id, 39 | 'name': name, 40 | 'description': description, 41 | 'command': command, 42 | 'role': role.index, 43 | 'addCommandToMessageList': addCommandToMessageList, 44 | 'bindOption': bindOption, 45 | }; 46 | } 47 | 48 | // 从JSON反序列化 49 | factory QuickCommand.fromJson(Map json) { 50 | return QuickCommand( 51 | id: json['id'] ?? 0, 52 | name: json['name'] ?? '', 53 | description: json['description'] ?? '', 54 | command: json['command'] ?? '', 55 | role: QuickCommandRole.values[json['role'] ?? 0], 56 | addCommandToMessageList: json['addCommandToMessageList'] ?? true, 57 | bindOption: json['bindOption'], 58 | ); 59 | } 60 | 61 | // 创建一个新的QuickCommand实例,使用现有的属性 62 | QuickCommand copyWith({ 63 | int? id, 64 | String? name, 65 | String? description, 66 | String? command, 67 | QuickCommandRole? role, 68 | bool? addCommandToMessageList, 69 | int? bindOption, 70 | }) { 71 | return QuickCommand( 72 | id: id ?? this.id, 73 | name: name ?? this.name, 74 | description: description ?? this.description, 75 | command: command ?? this.command, 76 | role: role ?? this.role, 77 | addCommandToMessageList: addCommandToMessageList ?? this.addCommandToMessageList, 78 | bindOption: bindOption ?? this.bindOption, 79 | ); 80 | } 81 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/chat-app/providers/web_session_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter_example/chat-app/models/character_model.dart'; 5 | import 'package:flutter_example/chat-app/models/chat_model.dart'; 6 | import 'package:flutter_example/chat-app/providers/character_controller.dart'; 7 | import 'package:flutter_example/chat-app/providers/chat_session_controller.dart'; 8 | import 'package:flutter_example/chat-app/utils/AIHandler.dart'; 9 | import 'package:flutter_example/chat-app/utils/entitys/ChatAIState.dart'; 10 | import 'package:flutter_example/chat-app/utils/entitys/RequestOptions.dart'; 11 | import 'package:flutter_example/chat-app/utils/entitys/llmMessage.dart'; 12 | import 'package:flutter_example/chat-app/utils/promptBuilder.dart'; 13 | import 'package:flutter_inappwebview/flutter_inappwebview.dart'; 14 | 15 | class WebSessionController { 16 | InAppWebViewController webViewController; 17 | ChatSessionController chatSessionController; 18 | final Function(dynamic args) onMessageEmit; 19 | 20 | WebSessionController( 21 | {required this.webViewController, 22 | required this.chatSessionController, 23 | required this.onMessageEmit}); 24 | 25 | void onWebViewCreated(InAppWebViewController controller) { 26 | chatSessionController.bindWebController(this); 27 | 28 | controller.addJavaScriptHandler( 29 | handlerName: 'fetchChat', 30 | callback: (args) { 31 | onChatChange(chatSessionController.chat); 32 | }); 33 | 34 | controller.addJavaScriptHandler( 35 | handlerName: 'fetchAllCharacters', 36 | callback: (args) { 37 | final charList = CharacterController.of.characters 38 | .map((c) => c.toJson(smallJson: true)) 39 | .toList(); 40 | return charList; 41 | }); 42 | 43 | controller.addJavaScriptHandler( 44 | handlerName: 'emitMessage', callback: (args) {}); 45 | } 46 | 47 | void onStateChange(ChatAIState newState) { 48 | print("ChatAIStateChange"); 49 | webViewController.evaluateJavascript( 50 | source: "window.onStateChange(${json.encode(newState.toJson())})"); 51 | } 52 | 53 | void onChatChange(ChatModel newChat) { 54 | print("window.onChatChange(${json.encode(newChat.toJson())})"); 55 | webViewController.evaluateJavascript( 56 | source: "window.onChatChange(${json.encode(newChat.toJson())})"); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /lib/chat-app/utils/markdown/latex_inline_syntax.dart: -------------------------------------------------------------------------------- 1 | import 'package:markdown/markdown.dart'; 2 | 3 | final List> delimiterList = [ 4 | {'left': r'$$', 'right': r'$$', 'display': true}, 5 | {'left': r'$', 'right': r'$', 'display': false}, 6 | {'left': r'\pu{', 'right': '}', 'display': false}, 7 | {'left': r'\ce{', 'right': '}', 'display': false}, 8 | {'left': r'\(', 'right': r'\)', 'display': false}, 9 | {'left': '( ', 'right': ' )', 'display': false}, 10 | {'left': r'\[', 'right': r'\]', 'display': true}, 11 | {'left': '[ ', 'right': ' ]', 'display': true}, 12 | ]; 13 | 14 | List inlinePatterns = []; 15 | List blockPatterns = []; 16 | 17 | String escapeRegex(String string) { 18 | return string.replaceAllMapped(RegExp(r'[-\/\\^$*+?.()|[\]{}]'), (match) { 19 | return '\\${match.group(0)}'; 20 | }); 21 | } 22 | 23 | String generateRegexRules(List> delimiters) { 24 | for (var delimiter in delimiters) { 25 | String left = delimiter['left']; 26 | String right = delimiter['right']; 27 | // Ensure regex-safe delimiters 28 | String escapedLeft = escapeRegex(left); 29 | String escapedRight = escapeRegex(right); 30 | 31 | // Inline pattern 32 | inlinePatterns.add( 33 | '$escapedLeft((?:\\\\.|[^\\\\\\n])*?(?:\\\\.|[^\\\\\\n]|(?!$escapedRight)))$escapedRight'); 34 | // Block pattern 35 | blockPatterns.add('$escapedLeft\\n((?:\\\\[^]|[^\\\\])+?)\\n$escapedRight'); 36 | } 37 | 38 | return '(${inlinePatterns.join("|")})(?=[\\s?!.,:?!。,:]|\$)'; 39 | } 40 | 41 | final _latexPattern = generateRegexRules(delimiterList); 42 | 43 | class LatexInlineSyntax extends InlineSyntax { 44 | LatexInlineSyntax() : super(_latexPattern); 45 | 46 | @override 47 | bool onMatch(InlineParser parser, Match match) { 48 | String raw = match.group(0) ?? ''; 49 | 50 | int delimiterLength = 1; 51 | String mathStyle = 'text'; 52 | // check delimiter 53 | for (var delimiter in delimiterList) { 54 | if (raw.startsWith(delimiter['left']) && 55 | raw.endsWith(delimiter['right'])) { 56 | mathStyle = delimiter['display'] ? 'display' : 'text'; 57 | delimiterLength = delimiter['left'].length; 58 | break; 59 | } 60 | } 61 | 62 | final equation = 63 | raw.substring(delimiterLength, raw.length - delimiterLength); 64 | 65 | final element = Element.text('latex', equation); 66 | element.attributes['MathStyle'] = mathStyle; 67 | parser.addNode(element); 68 | 69 | return true; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/chat-app/providers/prompt_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'package:flutter_example/chat-app/providers/setting_controller.dart'; 4 | import 'package:get/get.dart'; 5 | import '../models/prompt_model.dart'; 6 | 7 | class PromptController extends GetxController { 8 | final RxList prompts = [].obs; 9 | final String fileName = 'prompts.json'; 10 | 11 | 12 | @override 13 | void onInit() { 14 | super.onInit(); 15 | loadPrompts(); 16 | } 17 | 18 | // 从本地加载提示词数据 19 | Future loadPrompts() async { 20 | try { 21 | final directory = await Get.find().getVaultPath(); 22 | final file = File('${directory}/$fileName'); 23 | 24 | if (await file.exists()) { 25 | final String contents = await file.readAsString(); 26 | final List jsonList = json.decode(contents); 27 | prompts.value = 28 | jsonList.map((json) => PromptModel.fromJson(json)).toList(); 29 | } 30 | } catch (e) { 31 | print('加载提示词数据失败: $e'); 32 | } 33 | } 34 | 35 | // 保存提示词数据到本地 36 | Future savePrompts() async { 37 | try { 38 | final directory = await Get.find().getVaultPath(); 39 | final file = File('${directory}/$fileName'); 40 | 41 | final String jsonString = json.encode( 42 | prompts.where((prompt) => !prompt.isInChat).map((prompt) => prompt.toJson()).toList(), 43 | ); 44 | await file.writeAsString(jsonString); 45 | } catch (e) { 46 | print('保存提示词数据失败: $e'); 47 | } 48 | } 49 | 50 | // 添加新提示词 51 | Future addPrompt(PromptModel prompt) async { 52 | prompts.add(prompt); 53 | await savePrompts(); 54 | } 55 | 56 | // 更新提示词 57 | Future updatePrompt(PromptModel prompt) async { 58 | final index = prompts.indexWhere((p) => p.id == prompt.id); 59 | if (index != -1) { 60 | prompts[index] = prompt; 61 | await savePrompts(); 62 | } 63 | } 64 | 65 | // 删除提示词 66 | Future deletePrompt(int id) async { 67 | prompts.removeWhere((p) => p.id == id); 68 | await savePrompts(); 69 | } 70 | 71 | // 根据名称和角色获取提示词 72 | PromptModel? getPromptByNameAndRole(String name, String role) { 73 | return prompts.firstWhereOrNull((p) => p.name == name && p.role == role); 74 | } 75 | 76 | // 根据ID获取提示词 77 | PromptModel? getPromptById(int id) { 78 | return prompts.firstWhereOrNull((p) => p.id == id); 79 | } 80 | 81 | void reorderPrompts(int oldIndex, int newIndex) { 82 | final prompt = prompts.removeAt(oldIndex); 83 | prompts.insert(newIndex, prompt); 84 | update(); 85 | savePrompts(); 86 | } 87 | 88 | } -------------------------------------------------------------------------------- /lib/chat-app/utils/service_handlers/DeepSeekServiceHandler.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:dio/dio.dart' as dio; 5 | import 'package:dio/dio.dart'; 6 | 7 | import 'package:flutter_example/chat-app/utils/service_handlers/OpenAIServiceHandler.dart'; 8 | import 'package:http/http.dart'; 9 | 10 | class Deepseekservicehandler extends Openaiservicehandler { 11 | const Deepseekservicehandler() 12 | : super( 13 | baseUrl: 'https://api.deepseek.com', 14 | name: 'deepseek', 15 | defaultModelList: const [ 16 | 'deepseek-chat', 17 | 'deepseek-reasoner', 18 | 'DeepSeek-V3-0324', 19 | 'DeepSeek-R1-0528', 20 | 'DeepSeek-V3', 21 | 'DeepSeek-R1' 22 | ]); 23 | 24 | @override 25 | bool get canFetchBalance => true; 26 | 27 | @override 28 | Future fetchBalance(String apiKey) async { 29 | final url = 'https://api.deepseek.com/user/balance'; 30 | 31 | final Dio _dio = Dio(); 32 | 33 | try { 34 | // 设置请求头,通常使用 Bearer Token 形式 35 | final response = await _dio.get( 36 | url, 37 | options: dio.Options( 38 | headers: { 39 | 'Authorization': 'Bearer $apiKey', // 根据实际 API 要求调整 40 | }, 41 | ), 42 | ); 43 | 44 | if (response.statusCode == 200) { 45 | final data = response.data; 46 | 47 | // 解析 balance_infos 数组 48 | final balanceInfos = data['balance_infos'] as List; 49 | final markdownLines = []; 50 | 51 | for (final info in balanceInfos) { 52 | final currency = info['currency'] as String; 53 | final totalBalance = info['total_balance'] as String; 54 | final grantedBalance = info['granted_balance'] as String; 55 | final toppedUpBalance = info['topped_up_balance'] as String; 56 | 57 | markdownLines.add('### 🪙 $currency'); 58 | markdownLines.add('- **总可用余额**: `$totalBalance`'); 59 | markdownLines.add('- **赠金余额**: `$grantedBalance`'); 60 | markdownLines.add('- **充值余额**: `$toppedUpBalance`'); 61 | markdownLines.add(''); 62 | } 63 | 64 | // 可选:添加是否可用状态 65 | final isAvailable = data['is_available'] as bool; 66 | markdownLines.add('---'); 67 | markdownLines.add('> 💡 账户当前是否可用: ${isAvailable ? "✅ 是" : "❌ 否"}'); 68 | 69 | return markdownLines.join('\n'); 70 | } else { 71 | return '❌ 请求失败,状态码: ${response.statusCode}'; 72 | } 73 | } on DioException catch (e) { 74 | return '❌ 网络错误: ${e.message}'; 75 | } catch (e) { 76 | return '❌ 未知错误: $e'; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/chat-app/utils/service_handlers/ServiceHandlerFactory.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_example/chat-app/models/api_model.dart'; 2 | import 'package:flutter_example/chat-app/utils/service_handlers/DeepSeekServiceHandler.dart'; 3 | import 'package:flutter_example/chat-app/utils/service_handlers/GoogleServiceHandler.dart'; 4 | import 'package:flutter_example/chat-app/utils/service_handlers/KimiServiceHandler.dart'; 5 | import 'package:flutter_example/chat-app/utils/service_handlers/OpenAIServiceHandler.dart'; 6 | import 'package:flutter_example/chat-app/utils/service_handlers/ServiceHandler.dart'; 7 | import 'package:flutter_example/chat-app/utils/service_handlers/SiliconFlowServiceHandler.dart'; 8 | 9 | class Servicehandlerfactory { 10 | static const providers = { 11 | ServiceProvider.openai: Openaiservicehandler( 12 | baseUrl: 'https://api.openai.com/v1', 13 | name: 'openAI', 14 | defaultModelList: [ 15 | "gpt-3.5-turbo", 16 | "gpt-4", 17 | "gpt-4o", 18 | ]), 19 | ServiceProvider.deepseek: Deepseekservicehandler(), 20 | ServiceProvider.siliconflow: Siliconflowservicehandler(), 21 | ServiceProvider.custom_openai_compatible: 22 | Openaiservicehandler(baseUrl: '', name: '自定义', defaultModelList: []), 23 | ServiceProvider.google: Googleservicehandler( 24 | baseUrl: 'no need', 25 | name: 'google', 26 | defaultModelList: [ 27 | "gemini-2.5-pro", 28 | "gemini-2.5-pro-preview-06-05", 29 | "gemini-2.5-pro-preview-05-06", 30 | "gemini-2.5-pro-preview-03-25", 31 | "gemini-2.5-pro-exp-03-25", 32 | "gemini-2.5-flash", 33 | "gemini-2.5-flash-preview-05-20", 34 | "gemini-2.5-flash-preview-04-17", 35 | "gemini-2.5-lite-preview-06-17", 36 | "gemini-2.0-flash", 37 | "gemini-2.0-flash-lite", 38 | "gemini-1.5-flash", 39 | "gemini-1.5-flash-8b", 40 | "gemini-1.5-pro", 41 | "gemini-1.0-pro" 42 | ]), 43 | ServiceProvider.kimi: Kimiservicehandler(), 44 | }; 45 | 46 | static const defaultHandler = Openaiservicehandler( 47 | baseUrl: 'https://api.openai.com/v1', 48 | name: 'openAI', 49 | defaultModelList: [ 50 | "gpt-3.5-turbo", 51 | "gpt-4", 52 | "gpt-4o", 53 | ]); 54 | 55 | static Servicehandler getHandler(ServiceProvider service, 56 | {String? customURL}) { 57 | if (service == ServiceProvider.custom_openai_compatible && 58 | customURL != null) { 59 | return Openaiservicehandler( 60 | baseUrl: customURL, name: '自定义', defaultModelList: []); 61 | } 62 | return providers[service] ?? defaultHandler; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/chat-app/widgets/stack_avatar.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_example/chat-app/utils/image_utils.dart'; 5 | import 'package:flutter_example/chat-app/widgets/AvatarImage.dart'; 6 | 7 | class StackAvatar extends StatelessWidget { 8 | final List avatarUrls; 9 | final int maxDisplayCount; 10 | final double avatarSize; 11 | final double spacing; 12 | 13 | const StackAvatar({ 14 | Key? key, 15 | required this.avatarUrls, 16 | this.maxDisplayCount = 3, 17 | this.avatarSize = 45, 18 | this.spacing = 17, 19 | }) : super(key: key); 20 | 21 | double _calculateSpacing() { 22 | int len = avatarUrls.length; 23 | if (len == 2) { 24 | return spacing * 2; 25 | } 26 | return spacing; 27 | } 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | final displayCount = avatarUrls.length > maxDisplayCount 32 | ? maxDisplayCount 33 | : avatarUrls.length; 34 | final hasMore = avatarUrls.length > maxDisplayCount; 35 | final dynamicSpacing = _calculateSpacing(); 36 | 37 | return SizedBox( 38 | width: avatarSize + ((spacing + 10) * (maxDisplayCount - 1)), 39 | height: avatarSize + 4, 40 | child: Stack( 41 | children: [ 42 | ...List.generate(displayCount, (index) { 43 | return Positioned( 44 | left: index * dynamicSpacing, 45 | child: Container( 46 | decoration: BoxDecoration( 47 | shape: BoxShape.circle, 48 | border: Border.all( 49 | color: Theme.of(context).colorScheme.surface, 50 | width: 2, 51 | ), 52 | ), 53 | child: AvatarImage.round(avatarUrls[index], avatarSize / 2)), 54 | ); 55 | }), 56 | if (hasMore) 57 | Positioned( 58 | right: 0, 59 | bottom: 0, 60 | child: Container( 61 | padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), 62 | decoration: BoxDecoration( 63 | color: Theme.of(context).colorScheme.primary, //Colors.green, 64 | borderRadius: BorderRadius.circular(10), 65 | ), 66 | child: Text( 67 | '+${avatarUrls.length - maxDisplayCount}', 68 | style: TextStyle( 69 | color: Theme.of(context).colorScheme.onPrimary, 70 | fontSize: 10, 71 | fontWeight: FontWeight.bold, 72 | ), 73 | ), 74 | ), 75 | ), 76 | ], 77 | ), 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/chat-app/pages/character/more_firstmessage_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_example/chat-app/models/character_model.dart'; 3 | 4 | class MoreFirstMessagePage extends StatefulWidget { 5 | final CharacterModel character; 6 | 7 | const MoreFirstMessagePage({Key? key, required this.character}) 8 | : super(key: key); 9 | 10 | @override 11 | _MoreFirstMessagePageState createState() => _MoreFirstMessagePageState(); 12 | } 13 | 14 | class _MoreFirstMessagePageState extends State { 15 | late List messages; 16 | 17 | @override 18 | void initState() { 19 | super.initState(); 20 | messages = List.from(widget.character.moreFirstMessage); 21 | } 22 | 23 | void _addMessage() { 24 | setState(() { 25 | messages.add(''); 26 | }); 27 | } 28 | 29 | void _updateMessage(int index, String value) { 30 | setState(() { 31 | messages[index] = value; 32 | }); 33 | } 34 | 35 | void _deleteMessage(int index) { 36 | setState(() { 37 | messages.removeAt(index); 38 | }); 39 | } 40 | 41 | void _saveChanges() { 42 | widget.character.moreFirstMessage = List.from(messages); 43 | //Get.back(); // 返回上一页 44 | } 45 | 46 | @override 47 | Widget build(BuildContext context) { 48 | final theme = Theme.of(context); 49 | 50 | return PopScope( 51 | onPopInvokedWithResult: (didPop, result) { 52 | _saveChanges(); 53 | }, 54 | child: Scaffold( 55 | appBar: AppBar( 56 | title: const Text('更多开场白'), 57 | ), 58 | body: ListView.builder( 59 | itemCount: messages.length, 60 | itemBuilder: (context, index) { 61 | return Card( 62 | margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), 63 | child: ListTile( 64 | title: TextFormField( 65 | initialValue: messages[index], 66 | decoration: InputDecoration( 67 | labelText: '消息 ${index + 1}', 68 | labelStyle: TextStyle(color: theme.primaryColor), 69 | ), 70 | onChanged: (value) => _updateMessage(index, value), 71 | ), 72 | trailing: IconButton( 73 | icon: Icon(Icons.delete, color: theme.colorScheme.error), 74 | onPressed: () => _deleteMessage(index), 75 | ), 76 | ), 77 | ); 78 | }, 79 | ), 80 | floatingActionButton: FloatingActionButton( 81 | onPressed: _addMessage, 82 | child: const Icon(Icons.add), 83 | backgroundColor: theme.primaryColor, 84 | ), 85 | ), 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/chat-app/widgets/chat/example_chat.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_example/chat-app/models/chat_model.dart'; 3 | import 'package:flutter_example/chat-app/models/message_model.dart'; 4 | import 'package:flutter_example/chat-app/providers/character_controller.dart'; 5 | import 'package:flutter_example/chat-app/widgets/chat/message_bubble.dart'; 6 | import 'package:get/get.dart'; 7 | 8 | class ExampleChat extends StatelessWidget { 9 | ExampleChat({super.key}); 10 | 11 | final assistant = Get.find().characters[0].id; 12 | int get user => 0; 13 | 14 | Widget _buildMessageBubble( 15 | ChatModel chat, MessageModel message, MessageModel? lastMessage, 16 | {int index = 0, bool isNarration = false}) { 17 | return MessageBubble( 18 | chat: chat, 19 | message: message, 20 | isSelected: false, 21 | onTap: () {}, 22 | index: message.id, 23 | onLongPress: () {}, 24 | buildBottomButtons: (p1, p2) => SizedBox.shrink(), 25 | onUpdateChat: () {}); 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | final exampleChat = ChatModel( 31 | id: 0, 32 | name: 'Example', 33 | avatar: '', 34 | lastMessage: 'lastMessage', 35 | time: 'enn', 36 | messages: [ 37 | MessageModel( 38 | id: 1, 39 | content: '你在何处?', 40 | senderId: user, 41 | time: DateTime.now(), 42 | alternativeContent: [null]), 43 | MessageModel( 44 | id: 2, 45 | content: 46 | '*一个纤弱的影子,在朦胧的月光中,轻盈地飘过古老的石板路。*"我在时间的长河里,在回忆的岸边。你呢,是哪阵风,将你吹到了这无人问津的角落?"', 47 | senderId: assistant, 48 | time: DateTime.now(), 49 | alternativeContent: [null]), 50 | MessageModel( 51 | id: 3, 52 | content: '我在寻你。', 53 | senderId: user, 54 | time: DateTime.now(), 55 | alternativeContent: [null]), 56 | MessageModel( 57 | id: 4, 58 | content: 59 | '"世间万物皆有其时,为何独独寻我? "*影子停下了脚步,转过身来,那双如同深海般的眼眸,凝视着你。*"我不过是一缕被遗忘的思绪,一朵早已凋零的花。"', 60 | senderId: assistant, 61 | time: DateTime.now(), 62 | alternativeContent: [null]), 63 | MessageModel( 64 | id: 5, 65 | content: '因为你是诗。', 66 | senderId: user, 67 | time: DateTime.now(), 68 | alternativeContent: [null]), 69 | ]); 70 | 71 | return Column( 72 | children: [ 73 | ...exampleChat.messages.map((msg) { 74 | return _buildMessageBubble(exampleChat, msg, null); 75 | }) 76 | ], 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/chat-app/widgets/icon_switch_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class IconSwitchButton extends StatelessWidget { 4 | final bool value; 5 | final String label; 6 | final IconData icon; 7 | final ValueChanged onChanged; 8 | final double? width; 9 | final double? height; 10 | final double? iconSize; 11 | final EdgeInsetsGeometry? padding; 12 | 13 | const IconSwitchButton({ 14 | Key? key, 15 | required this.value, 16 | required this.label, 17 | required this.icon, 18 | required this.onChanged, 19 | this.width, 20 | this.height = 30, 21 | this.iconSize = 18, 22 | this.padding, 23 | }) : super(key: key); 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | final theme = Theme.of(context); 28 | final primaryColor = HSLColor.fromColor(theme.primaryColor) 29 | .withSaturation(0.85) 30 | .withLightness(0.5) 31 | .toColor(); 32 | final disabledColor = theme.disabledColor; 33 | 34 | return Material( 35 | color: Colors.transparent, 36 | child: InkWell( 37 | onTap: () => onChanged(!value), 38 | borderRadius: BorderRadius.circular(12), 39 | child: AnimatedContainer( 40 | duration: const Duration(milliseconds: 200), 41 | curve: Curves.easeInOut, 42 | width: width, 43 | height: height, 44 | padding: padding ?? const EdgeInsets.symmetric(horizontal: 12), 45 | decoration: BoxDecoration( 46 | color: value ? primaryColor.withOpacity(0.15) : disabledColor.withOpacity(0.08), 47 | borderRadius: BorderRadius.circular(32), 48 | ), 49 | child: Row( 50 | mainAxisSize: MainAxisSize.min, 51 | mainAxisAlignment: MainAxisAlignment.center, 52 | children: [ 53 | AnimatedSwitcher( 54 | duration: const Duration(milliseconds: 200), 55 | transitionBuilder: (Widget child, Animation animation) { 56 | return ScaleTransition(scale: animation, child: child); 57 | }, 58 | child: Icon( 59 | icon, 60 | key: ValueKey(value), 61 | size: iconSize, 62 | color: value ? primaryColor : disabledColor, 63 | ), 64 | ), 65 | const SizedBox(width: 8), 66 | AnimatedDefaultTextStyle( 67 | duration: const Duration(milliseconds: 200), 68 | style: TextStyle( 69 | color: value ? primaryColor : disabledColor, 70 | fontSize: 14, 71 | fontWeight: value ? FontWeight.w500 : FontWeight.normal, 72 | ), 73 | child: Text(label), 74 | ), 75 | ], 76 | ), 77 | ), 78 | ), 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/chat-app/widgets/webview/chat_webview.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_example/chat-app/providers/chat_session_controller.dart'; 5 | import 'package:flutter_example/chat-app/providers/web_session_controller.dart'; 6 | import 'package:flutter_example/chat-app/widgets/AvatarImage.dart'; 7 | import 'package:flutter_inappwebview/flutter_inappwebview.dart'; 8 | import 'package:get/get.dart'; 9 | import 'package:get/state_manager.dart'; 10 | import 'package:mime/mime.dart'; // 推荐引入 mime 包来动态获取 content-type 11 | 12 | class ChatWebview extends StatefulWidget { 13 | const ChatWebview( 14 | {super.key, required this.session, required this.onMessageEmit}); 15 | final ChatSessionController session; 16 | 17 | final Function(dynamic args) onMessageEmit; 18 | 19 | @override 20 | State createState() => _ChatWebviewState(); 21 | } 22 | 23 | class _ChatWebviewState extends State { 24 | late InAppWebViewController _webViewController; 25 | 26 | ChatSessionController get session => widget.session; 27 | 28 | late final webSessionController = WebSessionController( 29 | webViewController: _webViewController, 30 | chatSessionController: session, 31 | onMessageEmit: widget.onMessageEmit); 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | return Padding( 36 | padding: const EdgeInsets.only(top: 64), 37 | child: InAppWebView( 38 | initialUrlRequest: URLRequest(url: WebUri("http://localhost:5173/")), 39 | //initialData: InAppWebViewInitialData(data: _htmlContent), 40 | initialSettings: InAppWebViewSettings(resourceCustomSchemes: ['imgs']), 41 | onLoadResourceWithCustomScheme: (controller, request) async { 42 | if (request.url.scheme == 'imgs') { 43 | // 解析路径,移除 scheme 部分,得到实际文件路径 44 | // 注意:根据你的 HTML 写法,这里可能需要处理 /// 或者 // 45 | String filePath = AvatarImage.getPath(request.url.path); 46 | 47 | // 如果是 Android 绝对路径,可能需要适当调整 path 48 | // 例如: request.url.toString() 可能会把 /// 变成 / 49 | 50 | File file = File(filePath); 51 | 52 | if (await file.exists()) { 53 | var bytes = await file.readAsBytes(); 54 | var mimeType = lookupMimeType(filePath) ?? 'image/png'; 55 | 56 | // 3. 返回文件数据给 WebView 57 | return CustomSchemeResponse( 58 | data: bytes, 59 | contentType: mimeType, 60 | ); 61 | } else { 62 | print("无法获取${filePath}"); 63 | } 64 | } 65 | return null; // 如果文件不存在或出错,返回 null 66 | }, 67 | onWebViewCreated: (controller) { 68 | _webViewController = controller; 69 | webSessionController.onWebViewCreated(controller); 70 | }, 71 | onConsoleMessage: (controller, consoleMessage) { 72 | print(consoleMessage); 73 | }, 74 | ), 75 | ); 76 | } 77 | 78 | @override 79 | void dispose() { 80 | session.closeWebController(); 81 | super.dispose(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/chat-app/utils/service_handlers/SiliconFlowServiceHandler.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter_example/chat-app/utils/entitys/llmMessage.dart'; 3 | import 'package:flutter_example/chat-app/utils/service_handlers/OpenAIServiceHandler.dart'; 4 | 5 | class Siliconflowservicehandler extends Openaiservicehandler { 6 | const Siliconflowservicehandler() 7 | : super( 8 | baseUrl: 'https://api.siliconflow.cn/v1', 9 | name: "SiliconFlow", 10 | defaultModelList: const [ 11 | 12 | ] 13 | ); 14 | 15 | @override 16 | parseMessage(LLMMessage message) async { 17 | if (message.fileDirs.isNotEmpty) { 18 | // 先单独计算所有 image_url(压缩并 base64 编码),然后再构建返回内容 19 | final List imageContents = []; 20 | for (final path in message.fileDirs) { 21 | imageContents.add({ 22 | "type": "image_url", 23 | "image_url": {"url": await parseImage(path)} 24 | }); 25 | } 26 | 27 | return { 28 | "role": message.role, 29 | "content": [ 30 | { 31 | "type": "text", 32 | "text": message.content, 33 | }, 34 | ...imageContents, 35 | ], 36 | }; 37 | } 38 | return { 39 | "role": message.role, 40 | "content": message.content, 41 | }; 42 | } 43 | 44 | @override 45 | bool get canFetchBalance => true; 46 | 47 | @override 48 | Future fetchBalance(String apiKey) async { 49 | final url = 'https://api.siliconflow.cn/v1/user/info'; 50 | final dio = Dio(); 51 | 52 | try { 53 | final response = await dio.get( 54 | url, 55 | options: Options( 56 | headers: { 57 | 'Authorization': 'Bearer $apiKey', // 使用 Bearer Token 认证 58 | }, 59 | ), 60 | ); 61 | 62 | if (response.statusCode == 200) { 63 | final data = response.data; 64 | 65 | // 检查响应状态 66 | final status = data['status'] as bool?; 67 | if (status != true) { 68 | return '❌ 接口返回失败: ${data['code'] ?? '未知错误'} - ${data['message'] ?? '无消息'}'; 69 | } 70 | 71 | final userData = data['data'] as Map?; 72 | 73 | if (userData == null) { 74 | return '❌ 数据解析失败: 未找到 data 字段'; 75 | } 76 | 77 | // 提取余额数据 78 | final balance = userData['balance'] as String? ?? "0.00"; 79 | final chargeBalance = userData['chargeBalance'] as String? ?? "0.00"; 80 | final totalBalance = userData['totalBalance'] as String? ?? "0.00"; 81 | 82 | // 构建 Markdown 83 | final markdown = ''' 84 | ### 🤖 硅基流动 (SiliconFlow) 账户余额 85 | 86 | 87 | - **当前余额**: `$balance` 88 | - **充值余额**: `$chargeBalance` 89 | - **总余额**: `$totalBalance` 90 | 91 | --- 92 | ✅ 状态: 成功 93 | '''; 94 | 95 | return markdown; 96 | } else { 97 | return '❌ 请求失败,状态码: ${response.statusCode}'; 98 | } 99 | } on DioException catch (e) { 100 | return '❌ 网络错误: ${e.message}'; 101 | } catch (e) { 102 | return '❌ 未知错误: $e'; 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/chat-app/models/lorebook_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_example/chat-app/models/lorebook_item_model.dart'; 2 | 3 | enum LorebookType { 4 | world, // 全局世界书 5 | character, // 角色书 6 | memory, // 记忆书 7 | } 8 | 9 | class LorebookModel { 10 | final int id; 11 | final String name; 12 | final List items; 13 | final int scanDepth; 14 | final int maxToken; 15 | 16 | final LorebookType type; 17 | final Map metaData = {}; 18 | 19 | LorebookModel({ 20 | required this.id, 21 | required this.name, 22 | required this.items, 23 | required this.scanDepth, 24 | required this.maxToken, 25 | this.type = LorebookType.world, 26 | }); 27 | 28 | // fromJson 29 | factory LorebookModel.fromJson(Map json) { 30 | return LorebookModel( 31 | id: json['id'] as int, 32 | name: json['name'] as String, 33 | items: (json['items'] as List) 34 | .map((e) => LorebookItemModel.fromJson(e as Map)) 35 | .toList(), 36 | scanDepth: json['scanDepth'] as int, 37 | maxToken: json['maxToken'] as int, 38 | type: LorebookType.values.firstWhere( 39 | (e) => e.toString().split('.').last == json['type'], 40 | orElse: () => LorebookType.world), 41 | )..metaData.addAll(json['metaData'] ?? {}); 42 | } 43 | 44 | factory LorebookModel.emptyWorldBook() { 45 | return LorebookModel( 46 | id: DateTime.now().microsecondsSinceEpoch, 47 | name: '空白世界书', 48 | items: [], 49 | scanDepth: 4, 50 | maxToken: 8000, 51 | type: LorebookType.world); 52 | } 53 | 54 | factory LorebookModel.emptyCharacterBook() { 55 | return LorebookModel( 56 | id: DateTime.now().microsecondsSinceEpoch, 57 | name: '空白角色书', 58 | items: [], 59 | scanDepth: 4, 60 | maxToken: 8000, 61 | type: LorebookType.character); 62 | } 63 | 64 | factory LorebookModel.emptyMemoryBook() { 65 | return LorebookModel( 66 | id: DateTime.now().microsecondsSinceEpoch, 67 | name: '空白记忆书', 68 | items: [], 69 | scanDepth: 4, 70 | maxToken: 8000, 71 | type: LorebookType.memory); 72 | } 73 | 74 | // toJson 75 | Map toJson() { 76 | return { 77 | 'id': id, 78 | 'name': name, 79 | 'items': items.map((e) => e.toJson()).toList(), 80 | 'scanDepth': scanDepth, 81 | 'maxToken': maxToken, 82 | 'metaData': metaData, 83 | 'type': type.toString().split('.').last, 84 | }; 85 | } 86 | 87 | // copy method 88 | LorebookModel copyWith({ 89 | int? id, 90 | String? name, 91 | List? items, 92 | int? scanDepth, 93 | int? maxToken, 94 | Map? metaData, 95 | LorebookType? type, 96 | }) { 97 | return LorebookModel( 98 | id: id ?? this.id, 99 | name: name ?? this.name, 100 | items: items ?? List.from(this.items), 101 | scanDepth: scanDepth ?? this.scanDepth, 102 | maxToken: maxToken ?? this.maxToken, 103 | type: type ?? this.type, 104 | )..metaData.addAll(metaData ?? this.metaData); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 22 | 26 | 30 | 31 | 32 | 33 | 34 | 35 | 40 | 44 | 45 | 47 | 50 | 51 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /lib/chat-app/widgets/chat/member_selector.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_example/chat-app/pages/character/edit_character_page.dart'; 4 | import 'package:flutter_example/chat-app/utils/customNav.dart'; 5 | import 'package:flutter_example/chat-app/utils/image_utils.dart'; 6 | import 'package:get/get.dart'; 7 | import '../../../chat-app/models/character_model.dart'; 8 | import '../../../chat-app/providers/character_controller.dart'; 9 | 10 | class MemberSelector extends StatelessWidget { 11 | final List selectedMembers; 12 | final Function(int) onToggleMember; 13 | 14 | const MemberSelector({ 15 | Key? key, 16 | required this.selectedMembers, 17 | required this.onToggleMember, 18 | }) : super(key: key); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | final CharacterController characterController = Get.find(); 23 | 24 | return Obx(() { 25 | final allCharacters = characterController.characters; 26 | Map> groupedCharacters = {}; 27 | 28 | for (var character in allCharacters) { 29 | if (!groupedCharacters.containsKey(character.category)) { 30 | groupedCharacters[character.category] = []; 31 | } 32 | groupedCharacters[character.category]!.add(character); 33 | } 34 | 35 | return ListView.builder( 36 | itemCount: groupedCharacters.length, 37 | itemBuilder: (context, index) { 38 | String category = groupedCharacters.keys.elementAt(index); 39 | List characters = groupedCharacters[category]!; 40 | 41 | return Column( 42 | crossAxisAlignment: CrossAxisAlignment.start, 43 | children: [ 44 | Padding( 45 | padding: EdgeInsets.fromLTRB(16, 16, 16, 8), 46 | child: Text( 47 | category, 48 | style: Theme.of(context).textTheme.titleMedium, 49 | ), 50 | ), 51 | ...characters.map((character) { 52 | final isMember = selectedMembers.contains(character.id); 53 | return Row( 54 | children: [ 55 | Expanded( 56 | child: ListTile( 57 | leading: CircleAvatar( 58 | backgroundImage: 59 | ImageUtils.getProvider(character.avatar), 60 | ), 61 | title: Text(character.roleName), 62 | trailing: isMember 63 | ? Icon(Icons.check_circle, color: Colors.green) 64 | : null, 65 | onTap: () => onToggleMember(character.id), 66 | ), 67 | ), 68 | IconButton( 69 | onPressed: () { 70 | customNavigate( 71 | EditCharacterPage( 72 | characterId: character.id, 73 | ), 74 | context: context); 75 | }, 76 | icon: Icon(Icons.more_horiz)), 77 | ], 78 | ); 79 | }).toList(), 80 | ], 81 | ); 82 | }, 83 | ); 84 | }); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/chat-app/widgets/chat/character_wheel.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'dart:ui'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_example/chat-app/models/message_model.dart'; 5 | import 'package:flutter_example/chat-app/widgets/AvatarImage.dart'; 6 | import '../../models/character_model.dart'; 7 | 8 | class CharacterWheel extends StatelessWidget { 9 | final List characters; 10 | final double radius; 11 | final Function(CharacterModel) onCharacterSelected; 12 | 13 | CharacterWheel({ 14 | Key? key, 15 | required List characters, 16 | this.radius = 160, 17 | required this.onCharacterSelected, 18 | }) : characters = [ 19 | ...characters, 20 | ], 21 | super(key: key); 22 | 23 | Widget _buildAvatar(CharacterModel character) { 24 | if (character.messageStyle == MessageStyle.narration) { 25 | return Icon(Icons.chat); 26 | } else { 27 | return AvatarImage(fileName: character.avatar); 28 | } 29 | } 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | return SizedBox( 34 | width: radius * 2, 35 | height: radius * 2, 36 | child: Stack( 37 | children: [ 38 | // 背景圆 39 | ClipOval( 40 | child: BackdropFilter( 41 | filter: ImageFilter.blur( 42 | sigmaX: 10.0, 43 | sigmaY: 10.0, 44 | ), 45 | child: Container( 46 | decoration: BoxDecoration( 47 | shape: BoxShape.circle, 48 | color: Theme.of(context) 49 | .colorScheme 50 | .surfaceContainerHighest 51 | .withOpacity(0.5), 52 | ), 53 | ), 54 | ), 55 | ), 56 | // 角色头像 57 | ...List.generate(characters.length, (index) { 58 | final angle = 2 * pi * index / characters.length; 59 | final x = radius + radius * 0.7 * cos(angle); 60 | final y = radius + radius * 0.7 * sin(angle); 61 | 62 | return Positioned( 63 | left: x - 25, 64 | top: y - 35, // 向上调整位置以适应文字空间 65 | child: GestureDetector( 66 | onTap: () => onCharacterSelected(characters[index]), 67 | child: Column( 68 | children: [ 69 | Container( 70 | width: 50, 71 | height: 50, 72 | decoration: BoxDecoration( 73 | shape: BoxShape.circle, 74 | border: Border.all( 75 | color: Theme.of(context).colorScheme.primary, 76 | width: 2, 77 | ), 78 | ), 79 | child: ClipOval(child: _buildAvatar(characters[index])), 80 | ), 81 | const SizedBox(height: 4), 82 | Text( 83 | characters[index].roleName, 84 | style: Theme.of(context).textTheme.labelSmall?.copyWith( 85 | color: Theme.of(context).colorScheme.primary, 86 | ), 87 | ), 88 | ], 89 | ), 90 | ), 91 | ); 92 | }), 93 | ], 94 | ), 95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /assets/webview/relation_map/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 人物关系图 7 | 8 | 12 | 13 | 14 | 15 |
16 | 17 | 98 | 99 | -------------------------------------------------------------------------------- /lib/chat-app/providers/lorebook_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'package:flutter_example/chat-app/models/lorebook_item_model.dart'; 4 | import 'package:flutter_example/chat-app/models/lorebook_model.dart'; 5 | import 'package:flutter_example/chat-app/providers/setting_controller.dart'; 6 | import 'package:get/get.dart'; 7 | 8 | class LoreBookController extends GetxController { 9 | final RxList lorebooks = [].obs; 10 | 11 | final Rx lorebookItemClipboard = 12 | Rx(null); 13 | 14 | // 全局激活的世界书 15 | final RxList globalActivitedLoreBookIds = [].obs; 16 | List get globalActivitedLoreBooks => globalActivitedLoreBookIds 17 | .map((i) => getLorebookById(i)) 18 | .nonNulls 19 | .toList(); 20 | 21 | final String fileName = 'lorebooks.json'; 22 | 23 | @override 24 | void onInit() { 25 | super.onInit(); 26 | loadLorebooks(); 27 | } 28 | 29 | // 加载世界书和激活的世界书ID 30 | Future loadLorebooks() async { 31 | try { 32 | final directory = await Get.find().getVaultPath(); 33 | final file = File('${directory}/$fileName'); 34 | if (await file.exists()) { 35 | final String contents = await file.readAsString(); 36 | final Map jsonMap = json.decode(contents); 37 | final List lorebookList = jsonMap['lorebooks'] ?? []; 38 | final List activatedList = 39 | jsonMap['globalActivitedLoreBooks'] ?? []; 40 | lorebooks.value = 41 | lorebookList.map((json) => LorebookModel.fromJson(json)).toList(); 42 | globalActivitedLoreBookIds.value = activatedList.cast(); 43 | } 44 | } catch (e) { 45 | print('加载世界书失败: $e'); 46 | } 47 | } 48 | 49 | // 保存世界书和激活的世界书ID 50 | Future saveLorebooks() async { 51 | try { 52 | lorebooks.refresh(); 53 | 54 | final directory = await Get.find().getVaultPath(); 55 | final file = File('${directory}/$fileName'); 56 | final Map jsonMap = { 57 | 'lorebooks': lorebooks.map((lorebook) => lorebook.toJson()).toList(), 58 | 'globalActivitedLoreBooks': globalActivitedLoreBookIds.toList(), 59 | }; 60 | final String jsonString = json.encode(jsonMap); 61 | await file.writeAsString(jsonString); 62 | } catch (e) { 63 | print('保存世界书失败: $e'); 64 | } 65 | } 66 | 67 | // 添加世界书 68 | Future addLorebook(LorebookModel lorebook) async { 69 | lorebooks.add(lorebook); 70 | await saveLorebooks(); 71 | } 72 | 73 | // 更新世界书 74 | Future updateLorebook(LorebookModel lorebook) async { 75 | final index = lorebooks.indexWhere((l) => l.id == lorebook.id); 76 | if (index != -1) { 77 | lorebooks[index] = lorebook; 78 | await saveLorebooks(); 79 | } 80 | } 81 | 82 | // 删除世界书 83 | Future deleteLorebook(int id) async { 84 | lorebooks.removeWhere((l) => l.id == id); 85 | await saveLorebooks(); 86 | } 87 | 88 | // 根据ID获取世界书 89 | LorebookModel? getLorebookById(int id) { 90 | return lorebooks.firstWhereOrNull((l) => l.id == id); 91 | } 92 | 93 | void reorderLorebooks(int oldIndex, int newIndex) { 94 | final lorebook = lorebooks.removeAt(oldIndex); 95 | lorebooks.insert(newIndex, lorebook); 96 | update(); 97 | saveLorebooks(); 98 | } 99 | 100 | static LoreBookController get of => Get.find(); 101 | } 102 | -------------------------------------------------------------------------------- /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", "com.example" "\0" 93 | VALUE "FileDescription", "SillyChat" "\0" 94 | VALUE "FileVersion", VERSION_AS_STRING "\0" 95 | VALUE "InternalName", "SillyChat" "\0" 96 | VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0" 97 | VALUE "OriginalFilename", "SillyChat.exe" "\0" 98 | VALUE "ProductName", "SillyChat" "\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 | -------------------------------------------------------------------------------- /lib/chat-app/widgets/chat/character_executer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_example/chat-app/pages/character/edit_character_page.dart'; 4 | import 'package:flutter_example/chat-app/providers/vault_setting_controller.dart'; 5 | import 'package:flutter_example/chat-app/utils/customNav.dart'; 6 | import 'package:flutter_example/chat-app/utils/image_utils.dart'; 7 | import 'package:flutter_example/chat-app/widgets/AvatarImage.dart'; 8 | import 'package:get/get.dart'; 9 | import '../../../chat-app/models/character_model.dart'; 10 | import '../../../chat-app/providers/character_controller.dart'; 11 | 12 | class CharacterExecuter extends StatelessWidget { 13 | final Function(int) onToggleMember; 14 | 15 | const CharacterExecuter({ 16 | Key? key, 17 | required this.onToggleMember, 18 | }) : super(key: key); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | // 最近选择过的角色 23 | final List recentCharacter = VaultSettingController.of() 24 | .historyModel 25 | .value 26 | .characterHistory 27 | .map((id) => CharacterController.of.getCharacterById(id)) 28 | .where((c) => c != null) 29 | .cast() 30 | .toList(); 31 | 32 | // 全部分组角色 33 | final Map> allGroupedCharacters = 34 | CharacterController.of.groupedCharacters; 35 | 36 | final double avatarDiameter = 46; 37 | Widget buildItem(CharacterModel c) { 38 | return InkWell( 39 | onTap: () => onToggleMember(c.id), 40 | child: SizedBox( 41 | width: 60, 42 | child: Column( 43 | mainAxisSize: MainAxisSize.min, 44 | children: [ 45 | SizedBox( 46 | width: avatarDiameter, 47 | height: avatarDiameter, 48 | child: 49 | AvatarImage.round( 50 | c.avatar, (avatarDiameter / 2)), 51 | ), 52 | const SizedBox(height: 6), 53 | Text( 54 | c.roleName ?? c.remark ?? '', 55 | maxLines: 1, 56 | overflow: TextOverflow.ellipsis, 57 | style: const TextStyle(fontSize: 12), 58 | ), 59 | ], 60 | ), 61 | ), 62 | ); 63 | } 64 | 65 | Widget buildSection(String title, List items) { 66 | if (items.isEmpty) return const SizedBox.shrink(); 67 | return Padding( 68 | padding: const EdgeInsets.symmetric(vertical: 8), 69 | child: Column( 70 | crossAxisAlignment: CrossAxisAlignment.start, 71 | children: [ 72 | Text(title, style: const TextStyle(fontWeight: FontWeight.bold)), 73 | const SizedBox(height: 8), 74 | Wrap( 75 | spacing: 12, 76 | runSpacing: 12, 77 | children: items.map(buildItem).toList(), 78 | ), 79 | ], 80 | ), 81 | ); 82 | } 83 | 84 | final List sections = []; 85 | if (recentCharacter.isNotEmpty) { 86 | sections.add(buildSection('最近', recentCharacter)); 87 | sections.add(const Divider()); 88 | } 89 | 90 | allGroupedCharacters.forEach((group, list) { 91 | sections.add(buildSection(group, list)); 92 | }); 93 | 94 | return SingleChildScrollView( 95 | padding: const EdgeInsets.all(12), 96 | child: Column( 97 | crossAxisAlignment: CrossAxisAlignment.start, 98 | children: sections, 99 | ), 100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /lib/chat-app/widgets/toggleChip.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// 一个类似 Chip 的自定义开关按钮小组件。 4 | /// 5 | /// 功能特性: 6 | /// - 包含一个图标和文本标签。 7 | /// - 拥有一个细边框。 8 | /// - 点击时可以在“开启”和“关闭”状态之间切换。 9 | /// - 关闭状态时,背景色为浅灰色,内容为深灰色。 10 | /// - 当状态改变时,会触发一个回调函数。 11 | /// - 点击时有平滑的动画效果。 12 | class ToggleChip extends StatefulWidget { 13 | /// 初始状态是开启还是关闭。 14 | final bool initialValue; 15 | 16 | /// 显示的图标。 17 | final IconData? icon; 18 | 19 | /// 显示的文本。 20 | final String text; 21 | 22 | /// 状态切换时的回调函数,返回新的状态值。 23 | final ValueChanged onToggle; 24 | 25 | final bool asButton; 26 | 27 | const ToggleChip({ 28 | super.key, 29 | this.icon, 30 | required this.text, 31 | this.initialValue = false, 32 | this.asButton = false, 33 | required this.onToggle, 34 | }); 35 | 36 | @override 37 | State createState() => _ToggleChipState(); 38 | } 39 | 40 | class _ToggleChipState extends State { 41 | late bool _isSelected; 42 | 43 | @override 44 | void initState() { 45 | super.initState(); 46 | _isSelected = widget.initialValue; 47 | } 48 | 49 | void _handleTap() { 50 | if (!widget.asButton) { 51 | setState(() { 52 | _isSelected = !_isSelected; 53 | }); 54 | } 55 | 56 | widget.onToggle(_isSelected); 57 | } 58 | 59 | @override 60 | Widget build(BuildContext context) { 61 | final theme = Theme.of(context); 62 | 63 | final activeColor = theme.primaryColor; 64 | final inactiveColor = theme.colorScheme.outline; 65 | final activeContentColor = theme.primaryColor; 66 | final inactiveBackgroundColor = theme.colorScheme.outline; 67 | 68 | return Padding( 69 | padding: const EdgeInsets.symmetric(horizontal: 4), 70 | child: GestureDetector( 71 | onTap: _handleTap, 72 | child: AnimatedContainer( 73 | duration: const Duration(milliseconds: 200), 74 | curve: Curves.easeInOut, 75 | padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), 76 | decoration: BoxDecoration( 77 | color: _isSelected 78 | ? activeColor.withOpacity(0.2) 79 | : inactiveBackgroundColor.withOpacity(0.2), 80 | borderRadius: BorderRadius.circular(12.0), 81 | border: Border.all( 82 | color: _isSelected ? activeColor : inactiveColor, 83 | width: 1.0, 84 | ), 85 | ), 86 | child: Row( 87 | mainAxisSize: MainAxisSize.min, 88 | children: [ 89 | if (widget.icon != null) 90 | AnimatedSwitcher( 91 | duration: const Duration(milliseconds: 200), 92 | transitionBuilder: (child, animation) => 93 | ScaleTransition(scale: animation, child: child), 94 | child: Icon( 95 | widget.icon, 96 | key: ValueKey(_isSelected), 97 | color: _isSelected ? activeContentColor : inactiveColor, 98 | size: 16.0, 99 | ), 100 | ), 101 | if (!(widget.icon == null || widget.text.isEmpty)) 102 | const SizedBox(width: 8.0), 103 | AnimatedDefaultTextStyle( 104 | duration: const Duration(milliseconds: 200), 105 | style: TextStyle( 106 | fontSize: 12, 107 | color: _isSelected ? activeContentColor : inactiveColor, 108 | ), 109 | child: Text(widget.text), 110 | ), 111 | ], 112 | ), 113 | ), 114 | ), 115 | ); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /lib/chat-app/providers/chat_option_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'package:get/get.dart'; 4 | import '../models/chat_option_model.dart'; 5 | import 'setting_controller.dart'; 6 | 7 | class ChatOptionController extends GetxController { 8 | final RxList chatOptions = [].obs; 9 | final String fileName = 'chat_options.json'; 10 | 11 | ChatOptionModel get defaultOption => 12 | chatOptions.isEmpty ? ChatOptionModel.roleplay() : chatOptions[0]; 13 | 14 | @override 15 | void onInit() { 16 | super.onInit(); 17 | loadChatOptions(); 18 | } 19 | 20 | // 从本地加载聊天选项数据 21 | Future loadChatOptions() async { 22 | try { 23 | final directory = await Get.find().getVaultPath(); 24 | final file = File('${directory}/$fileName'); 25 | 26 | if (await file.exists()) { 27 | final String contents = await file.readAsString(); 28 | final dynamic jsonData = json.decode(contents); 29 | 30 | // 兼容老数据格式(数组),新格式为对象包含 chatOptions 字段 31 | List jsonList; 32 | if (jsonData is List) { 33 | // 老数据格式,迁移为新格式 34 | jsonList = jsonData; 35 | // (先不)保存为新格式 36 | // await saveChatOptions(); 37 | } else if (jsonData is Map && jsonData['chatOptions'] is List) { 38 | jsonList = jsonData['chatOptions']; 39 | } else { 40 | jsonList = []; 41 | } 42 | 43 | chatOptions.value = 44 | jsonList.map((json) => ChatOptionModel.fromJson(json)).toList(); 45 | } 46 | } catch (e) { 47 | Get.snackbar("加载聊天预设数据失败", "$e"); 48 | print('加载聊天选项数据失败: $e'); 49 | } 50 | } 51 | 52 | // 保存聊天选项数据到本地 53 | Future saveChatOptions() async { 54 | try { 55 | final directory = await Get.find().getVaultPath(); 56 | final file = File('${directory}/$fileName'); 57 | 58 | final String jsonString = json.encode({ 59 | 'chatOptions': chatOptions.map((option) => option.toJson()).toList(), 60 | }); 61 | await file.writeAsString(jsonString); 62 | } catch (e) { 63 | Get.snackbar("保存聊天预设数据失败", "$e"); 64 | print('保存聊天选项数据失败: $e'); 65 | } 66 | } 67 | 68 | // 添加新聊天选项 69 | Future addChatOption(ChatOptionModel chatOption) async { 70 | chatOptions.add(chatOption); 71 | await saveChatOptions(); 72 | } 73 | 74 | // 更新聊天选项 75 | Future updateChatOption(ChatOptionModel chatOption, int? index) async { 76 | if (index == null) { 77 | index = chatOptions.indexWhere((option) => option.id == chatOption.id); 78 | } 79 | if (index >= 0 && index < chatOptions.length) { 80 | chatOptions[index] = chatOption; 81 | await saveChatOptions(); 82 | } 83 | } 84 | 85 | // 删除聊天选项 86 | Future deleteChatOption(int index) async { 87 | if (index >= 0 && index < chatOptions.length) { 88 | chatOptions.removeAt(index); 89 | await saveChatOptions(); 90 | } 91 | } 92 | 93 | // 获取特定索引的聊天选项 94 | ChatOptionModel? getChatOptionByIndex(int index) { 95 | if (index >= 0 && index < chatOptions.length) { 96 | return chatOptions[index]; 97 | } 98 | return null; 99 | } 100 | 101 | ChatOptionModel? getChatOptionById(int id) { 102 | return chatOptions.firstWhereOrNull((option) => option.id == id); 103 | } 104 | 105 | // 重新排序聊天选项 106 | void reorderChatOptions(int oldIndex, int newIndex) { 107 | final option = chatOptions.removeAt(oldIndex); 108 | chatOptions.insert(newIndex, option); 109 | update(); 110 | saveChatOptions(); 111 | } 112 | 113 | static ChatOptionController of() { 114 | return Get.find(); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lib/chat-app/utils/image_packer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:typed_data'; 3 | import 'package:archive/archive.dart'; 4 | import 'package:path/path.dart' as path; 5 | import 'package:path_provider/path_provider.dart'; 6 | import 'package:encrypt/encrypt.dart' as encrypt; 7 | 8 | class ImagePacker { 9 | static const String _outputDir = '.unpacked_images'; 10 | // 使用固定的加密密钥(32字符,256位) 11 | static const String _encryptionKey = 'xK8#mP2\$vL5*nQ9@wX4&jY7!zC3^hN6%'; 12 | static final _encrypter = encrypt.Encrypter( 13 | encrypt.AES(encrypt.Key.fromUtf8(_encryptionKey))); 14 | static final _iv = encrypt.IV.fromLength(16); 15 | 16 | /// 获取应用程序专用的存储目录 17 | static Future _getStorageDir() async { 18 | return (await getApplicationDocumentsDirectory()).path; 19 | } 20 | 21 | /// 加密数据 22 | // ignore: unused_element 23 | static Uint8List _encryptData(List data) { 24 | final encrypted = _encrypter.encryptBytes(data, iv: _iv); 25 | return encrypted.bytes; 26 | } 27 | 28 | /// 解密数据 29 | // ignore: unused_element 30 | static Uint8List _decryptData(List data) { 31 | final encrypted = encrypt.Encrypted(Uint8List.fromList(data)); 32 | return Uint8List.fromList(_encrypter.decryptBytes(encrypted, iv: _iv)); 33 | } 34 | 35 | /// 打包图片文件 36 | /// [imageMap] 图片ID和路径的映射关系 37 | /// [outputPath] 输出zip文件的路径 38 | /// 返回是否打包成功 39 | static Future packImages( 40 | Map imageMap, String outputPath) async { 41 | try { 42 | final archive = Archive(); 43 | 44 | for (var entry in imageMap.entries) { 45 | final file = File(entry.value); 46 | if (!await file.exists()) continue; 47 | 48 | final bytes = await file.readAsBytes(); 49 | final extension = path.extension(entry.value); 50 | final archiveFile = ArchiveFile( 51 | '${entry.key}$extension', 52 | bytes.length, 53 | bytes, 54 | ); 55 | archive.addFile(archiveFile); 56 | } 57 | 58 | final zipData = ZipEncoder().encode(archive); 59 | if (zipData == null) return false; 60 | 61 | // 加密并保存文件 62 | final encryptedData = zipData; //_encryptData(zipData); 63 | await File(outputPath).writeAsBytes(encryptedData); 64 | return true; 65 | } catch (e) { 66 | print('打包失败: $e'); 67 | return false; 68 | } 69 | } 70 | 71 | /// 解包图片文件 72 | /// [zipPath] zip文件路径 73 | /// [baseDir] 解压基础目录,默认为当前目录 74 | /// 返回图片ID和解压后路径的映射关系 75 | static Future> unpackImages( 76 | String zipPath, {String? baseDir}) async { 77 | try { 78 | // 读取并解密文件 79 | //final encryptedBytes = await File(zipPath).readAsBytes(); 80 | final decryptedBytes = await File(zipPath).readAsBytes();//_decryptData(encryptedBytes); 81 | final archive = ZipDecoder().decodeBytes(decryptedBytes); 82 | 83 | // 获取跨平台存储目录 84 | final basePath = baseDir ?? await _getStorageDir(); 85 | final outputDir = path.join(basePath, _outputDir); 86 | await Directory(outputDir).create(recursive: true); 87 | 88 | final Map resultMap = {}; 89 | 90 | for (final file in archive) { 91 | final filename = file.name; 92 | if (file.isFile) { 93 | final data = file.content as List; 94 | final filePath = path.join(outputDir, filename); 95 | await File(filePath).writeAsBytes(data); 96 | 97 | final id = path.basenameWithoutExtension(filename); 98 | resultMap[id] = filePath; 99 | } 100 | } 101 | 102 | return resultMap; 103 | } catch (e) { 104 | print('解包失败: $e'); 105 | return {}; 106 | } 107 | } 108 | } 109 | 110 | 111 | -------------------------------------------------------------------------------- /lib/chat-app/utils/sillyTavern/STLorebookImporter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_example/chat-app/models/lorebook_item_model.dart'; 2 | import 'package:flutter_example/chat-app/models/lorebook_model.dart'; 3 | 4 | int? parseMotherFuckerToInt(dynamic motherFucker) { 5 | if (motherFucker == null) return null; 6 | if (motherFucker is int) return motherFucker; 7 | if (motherFucker is double || motherFucker is num) 8 | return motherFucker.toInt(); 9 | if (motherFucker is String) { 10 | return int.tryParse(motherFucker) ?? double.tryParse(motherFucker)?.toInt(); 11 | } 12 | return null; 13 | } 14 | 15 | abstract class STLorebookImporter { 16 | static String getPositionByString(String depthName) { 17 | switch (depthName) { 18 | case 'before char': 19 | case 'after char': 20 | return depthName.replaceAll(' ', '_'); 21 | default: 22 | return depthName; 23 | } 24 | } 25 | 26 | static LorebookModel? fromJson(Map json, 27 | {String? fileName, LorebookType type = LorebookType.world}) { 28 | LorebookModel lorebook = LorebookModel( 29 | id: DateTime.now().microsecondsSinceEpoch, 30 | name: json['name'] ?? fileName ?? "名称未知的世界书", 31 | items: [], 32 | scanDepth: 4, 33 | maxToken: 99999, 34 | type: type); 35 | 36 | List entries; 37 | if (json['entries'] is List) { 38 | entries = json['entries']; 39 | } else if (json['entries'] is Map) { 40 | entries = (json['entries'] as Map).values.toList(); 41 | } else { 42 | entries = []; 43 | } 44 | 45 | entries.sort((a, b) { 46 | int aIndex = (a['extensions']?['insertion_order'] ?? 0) as int; 47 | int bIndex = (b['extensions']?['insertion_order'] ?? 0) as int; 48 | return bIndex.compareTo(aIndex); 49 | }); 50 | 51 | int index = 0; 52 | entries.forEach((entry) { 53 | Map? extensions = entry['extensions']; 54 | 55 | ActivationType type = ActivationType.manual; 56 | if (entry['constant'] == true) { 57 | type = ActivationType.always; 58 | } else { 59 | type = ActivationType.keywords; 60 | } 61 | 62 | dynamic position = extensions?['position'] ?? entry['position']; 63 | String lorePosition = ''; 64 | if (position is String) { 65 | lorePosition = getPositionByString(position); 66 | } else if (position is int) { 67 | lorePosition = [ 68 | 'before_char', 69 | 'after_char', 70 | '@Duser', 71 | '@Duser', // 這兩種對於沒人用的“作者注釋之前/之後” 72 | '@D', 73 | 'before_em', 74 | 'after_em' 75 | ][position] ?? 76 | 'before char'; 77 | if (position == 4) { 78 | lorePosition += 79 | ['system', 'user', 'assistant'][(extensions?['role'] ?? 1)]; 80 | } 81 | } 82 | 83 | LorebookItemModel item = LorebookItemModel( 84 | id: index, 85 | name: entry['comment'], 86 | content: entry['content'], 87 | priority: parseMotherFuckerToInt(entry['insertion_order']) ?? 100, 88 | isActive: entry['enabled'] ?? !entry['disable'], 89 | activationDepth: parseMotherFuckerToInt(entry['scanDepth']) ?? 0, 90 | position: lorePosition, 91 | positionId: (position == 3 || position == 2) 92 | ? 4 93 | : (parseMotherFuckerToInt(extensions?['depth']) ?? 1), 94 | keywords: 95 | ((entry['keys'] ?? entry['key'] ?? []) as List).join(','), 96 | activationType: type, 97 | ); 98 | lorebook.items.add(item); 99 | 100 | index++; 101 | }); 102 | 103 | return lorebook; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lib/chat-app/models/prompt_model.dart: -------------------------------------------------------------------------------- 1 | class PromptModel { 2 | int id; 3 | String content; 4 | String role; 5 | DateTime createDate; 6 | DateTime updateDate; 7 | String name; 8 | 9 | bool isInChat = false; 10 | 11 | bool isEnable = true; 12 | 13 | int priority; 14 | int depth; // prompt排序,0代表最新消息之后,1代表最新消息之前 15 | 16 | bool isChatHistory; // 占位符,在提示词列表中代表整个消息列表 17 | 18 | PromptModel({ 19 | required this.id, 20 | required this.content, 21 | required this.role, 22 | required this.name, 23 | DateTime? createDate, 24 | DateTime? updateDate, 25 | bool this.isInChat = false, 26 | bool this.isChatHistory = false, 27 | this.priority = 100, 28 | this.depth = 4, 29 | }) : this.createDate = createDate ?? DateTime.now(), 30 | this.updateDate = updateDate ?? DateTime.now(); 31 | 32 | PromptModel.chatHistoryPlaceholder() 33 | : id = 0, 34 | content = 'messageList', 35 | role = '', 36 | name = '消息列表', 37 | isEnable = true, 38 | createDate = DateTime.now(), 39 | updateDate = DateTime.now(), 40 | isChatHistory = true, 41 | priority = 100, 42 | depth = 4; 43 | 44 | PromptModel.userMessagePlaceholder() 45 | : id = -1, 46 | content = '{{lastuserMessage}}', 47 | role = 'user', 48 | name = '用户消息', 49 | isEnable = true, 50 | createDate = DateTime.now(), 51 | updateDate = DateTime.now(), 52 | isChatHistory = false, 53 | priority = 100, 54 | depth = 4; 55 | 56 | PromptModel.fromJson(Map json) 57 | : id = json['id'], 58 | content = json['content'], 59 | role = json['role'], 60 | name = json['name'], 61 | createDate = DateTime.parse(json['createDate']), 62 | updateDate = DateTime.parse(json['updateDate']), 63 | isEnable = json['isEnable'] ?? true, 64 | priority = json['priority'] ?? 100, 65 | depth = json['depth'] ?? 4, 66 | isInChat = json['isInChat'] ?? false, 67 | isChatHistory = json['isMessageList'] ?? false; 68 | 69 | Map toJson() => { 70 | 'id': id, 71 | 'content': content, 72 | 'role': role, 73 | 'name': name, 74 | 'createDate': createDate.toIso8601String(), 75 | 'updateDate': updateDate.toIso8601String(), 76 | 'isEnable': isEnable, 77 | 'priority': priority, 78 | 'depth': depth, 79 | 'isInChat': isInChat, 80 | 'isMessageList': isChatHistory 81 | }; 82 | 83 | PromptModel copy() { 84 | return PromptModel( 85 | id: id, 86 | content: content, 87 | role: role, 88 | name: name, 89 | createDate: createDate, 90 | updateDate: updateDate, 91 | isInChat: isInChat, 92 | isChatHistory: isChatHistory, 93 | depth: depth, 94 | priority: priority, 95 | )..isEnable = isEnable; 96 | } 97 | 98 | PromptModel copyWith({ 99 | int? id, 100 | String? content, 101 | String? role, 102 | String? name, 103 | DateTime? createDate, 104 | DateTime? updateDate, 105 | bool? isInChat, 106 | bool? isEnable, 107 | int? priority, 108 | int? depth, 109 | }) { 110 | return PromptModel( 111 | id: id ?? this.id, 112 | content: content ?? this.content, 113 | role: role ?? this.role, 114 | name: name ?? this.name, 115 | createDate: createDate ?? this.createDate, 116 | updateDate: updateDate ?? this.updateDate, 117 | isInChat: isInChat ?? this.isInChat, 118 | isChatHistory: this.isChatHistory, // 保持isMessageList不变 119 | ) 120 | ..isEnable = isEnable ?? this.isEnable 121 | ..priority = priority ?? this.priority 122 | ..depth = depth ?? this.depth; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /lib/chat-app/widgets/BreadcrumbNavigation.dart: -------------------------------------------------------------------------------- 1 | // 定义回调函数的类型签名 2 | import 'package:flutter/material.dart'; 3 | 4 | typedef OnCrumbTap = void Function(String path); 5 | 6 | /// 一个可配置的面包屑导航组件 7 | class BreadcrumbNavigation extends StatelessWidget { 8 | /// 当前的完整路径, e.g., "a/b/c/d" 9 | final String path; 10 | 11 | /// 根路径, e.g., "a/b" 12 | final String basePath; 13 | 14 | /// 点击面包屑项时的回调函数 15 | final OnCrumbTap onCrumbTap; 16 | 17 | /// 自定义根路径的显示名称 18 | final String rootLabel; 19 | 20 | /// 自定义分隔符 21 | final Widget separator; 22 | 23 | /// 自定义文本样式 24 | final TextStyle? style; 25 | 26 | /// 自定义激活状态的文本样式(最后一个面包屑) 27 | final TextStyle? activeStyle; 28 | 29 | /// 最多显示的面包屑层级数 30 | final int maxLevels; 31 | 32 | const BreadcrumbNavigation({ 33 | Key? key, 34 | required this.path, 35 | required this.basePath, 36 | required this.onCrumbTap, 37 | this.rootLabel = '根路径', // 默认显示为 "根路径" 38 | this.separator = const Icon(Icons.chevron_right, size: 18.0), 39 | this.style, 40 | this.activeStyle, 41 | this.maxLevels = 3, // 默认最多显示3级 42 | }) : super(key: key); 43 | 44 | @override 45 | Widget build(BuildContext context) { 46 | // _buildCrumbs 方法负责解析路径并返回面包屑数据列表 47 | final items = _buildCrumbs(); 48 | 49 | // 如果没有可显示的面包屑,则返回一个空容器 50 | if (items.isEmpty) { 51 | return Container(); 52 | } 53 | 54 | // 使用 ListView 或 SingleChildScrollView 来防止内容溢出 55 | return SingleChildScrollView( 56 | reverse: true, 57 | scrollDirection: Axis.horizontal, 58 | child: Row( 59 | mainAxisAlignment: MainAxisAlignment.start, 60 | children: List.generate(items.length * 2 - 1, (index) { 61 | if (index.isEven) { 62 | // 偶数索引是面包屑项 63 | final itemIndex = index ~/ 2; 64 | final item = items[itemIndex]; 65 | final isLast = itemIndex == items.length - 1; 66 | 67 | return _buildCrumbItem(item, isLast); 68 | } else { 69 | // 奇数索引是分隔符 70 | return separator; 71 | } 72 | }), 73 | ), 74 | ); 75 | } 76 | 77 | /// 构建单个面包屑项 78 | Widget _buildCrumbItem(_BreadcrumbItem item, bool isLast) { 79 | // 最后一个面包屑项通常是当前页面,可以设置为不可点击或有不同样式 80 | return InkWell( 81 | onTap: isLast ? null : () => onCrumbTap(item.path), 82 | child: Padding( 83 | padding: const EdgeInsets.all(8.0), 84 | child: Text( 85 | item.label, 86 | style: isLast 87 | ? activeStyle ?? style?.copyWith(fontWeight: FontWeight.bold) 88 | : style, 89 | ), 90 | ), 91 | ); 92 | } 93 | 94 | /// 解析路径并生成面包屑数据 95 | List<_BreadcrumbItem> _buildCrumbs() { 96 | // 检查路径是否合法 97 | if (!path.startsWith(basePath)) { 98 | // 如果当前路径不在根路径下,不显示面包屑 99 | return []; 100 | } 101 | 102 | final List<_BreadcrumbItem> items = []; 103 | 104 | // 1. 添加根路径面包屑 105 | items.add(_BreadcrumbItem(label: rootLabel, path: basePath)); 106 | 107 | // 2. 处理剩余路径 108 | // e.g., path="a/b/c/d", basePath="a/b" -> remaining="c/d" 109 | String remainingPath = path.substring(basePath.length); 110 | if (remainingPath.startsWith('/')) { 111 | remainingPath = remainingPath.substring(1); 112 | } 113 | 114 | // 如果没有剩余路径,直接返回根路径 115 | if (remainingPath.isEmpty) { 116 | return items; 117 | } 118 | 119 | final segments = remainingPath.split('/'); 120 | 121 | // 3. 逐级生成面包屑项 122 | String currentPath = basePath; 123 | for (final segment in segments) { 124 | currentPath = '$currentPath/$segment'; 125 | items.add(_BreadcrumbItem(label: segment, path: currentPath)); 126 | } 127 | 128 | // 4. 根据 maxLevels 截取末尾的N个面包屑 129 | if (items.length > maxLevels) { 130 | return items.sublist(items.length - maxLevels); 131 | } 132 | 133 | return items; 134 | } 135 | } 136 | 137 | /// 用于存储每个面包屑项的数据模型 138 | class _BreadcrumbItem { 139 | final String label; 140 | final String path; 141 | 142 | _BreadcrumbItem({required this.label, required this.path}); 143 | } 144 | -------------------------------------------------------------------------------- /lib/chat-app/models/chat_metadata_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_example/chat-app/models/character_model.dart'; 2 | import 'package:flutter_example/chat-app/models/chat_model.dart'; 3 | import 'package:flutter_example/chat-app/pages/chat/chat_page.dart'; 4 | import 'package:flutter_example/chat-app/providers/character_controller.dart'; 5 | import 'package:get/get.dart'; 6 | 7 | class ChatMetaModel { 8 | late final int id; 9 | late final String name; 10 | //late final String avatar; 11 | late final String lastMessage; 12 | late final String time; 13 | late final int messageCount; 14 | late final List characterIds; 15 | late final int assistantId; 16 | late final ChatMode mode; 17 | 18 | // JSON Ignore 19 | late final String path; 20 | 21 | ChatMetaModel({ 22 | required this.id, 23 | required this.name, 24 | //required this.avatar, 25 | required this.lastMessage, 26 | required this.time, 27 | required this.messageCount, 28 | required this.characterIds, 29 | required this.assistantId, 30 | required this.mode, 31 | }); 32 | 33 | CharacterModel get assistant { 34 | CharacterController controller = Get.find(); 35 | return controller.getCharacterById(assistantId); 36 | } 37 | 38 | List getAllAvatars() { 39 | final controller = CharacterController.of; 40 | return characterIds 41 | .map((id) => controller.getCharacterById(id)) 42 | .map((char) => char.avatar) 43 | .toList(); 44 | } 45 | 46 | factory ChatMetaModel.fromChatModel(ChatModel chatModel) { 47 | return ChatMetaModel( 48 | id: chatModel.id, 49 | name: chatModel.name, 50 | //avatar: chatModel.assistant.avatar, 51 | lastMessage: chatModel.lastMessage, 52 | time: chatModel.time, 53 | messageCount: chatModel.messages.length, 54 | characterIds: chatModel.characterIds, 55 | mode: chatModel.mode ?? ChatMode.auto, 56 | assistantId: chatModel.assistantId ?? -1); 57 | } 58 | 59 | factory ChatMetaModel.fromJson(Map json) { 60 | return ChatMetaModel( 61 | id: json['id'], 62 | name: json['name'], 63 | //avatar: json['avatar'], 64 | lastMessage: json['lastMessage'], 65 | time: json['time'], 66 | messageCount: json['messageCount'], 67 | characterIds: (json['characterIds'] as List?)?.cast() ?? [], 68 | assistantId: json['assistant'], 69 | mode: json['mode'] != null 70 | ? ChatMode.values.firstWhere( 71 | (e) => e.toString() == 'ChatMode.${json['mode']}', 72 | orElse: () => ChatMode.auto) 73 | : ChatMode.auto); 74 | } 75 | 76 | Map toJson() { 77 | return { 78 | 'id': id, 79 | 'name': name, 80 | //'avatar': avatar, 81 | 'lastMessage': lastMessage, 82 | 'time': time, 83 | 'messageCount': messageCount, 84 | 'characterIds': characterIds, 85 | 'assistant': assistantId, 86 | 'mode': mode.toString().split('.').last, 87 | }; 88 | } 89 | 90 | List get characters { 91 | CharacterController controller = Get.find(); 92 | return characterIds 93 | .map((id) => controller.getCharacterById(id)) 94 | .nonNulls 95 | .toList(); 96 | } 97 | 98 | ChatMetaModel copyWith({ 99 | int? id, 100 | String? name, 101 | String? backgroundImage, 102 | String? lastMessage, 103 | String? time, 104 | int? messageCount, 105 | List? characterIds, 106 | int? assistant, 107 | ChatMode? mode, 108 | String? path, 109 | }) { 110 | return ChatMetaModel( 111 | id: id ?? this.id, 112 | name: name ?? this.name, 113 | lastMessage: lastMessage ?? this.lastMessage, 114 | time: time ?? this.time, 115 | messageCount: messageCount ?? this.messageCount, 116 | characterIds: characterIds ?? this.characterIds, 117 | mode: mode ?? this.mode, 118 | assistantId: assistant ?? this.assistantId, 119 | )..path = path ?? this.path; 120 | } 121 | } 122 | --------------------------------------------------------------------------------