├── .gitattributes ├── .gitignore ├── .metadata ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── hahastudio │ │ │ │ └── flutterchat │ │ │ │ └── flutter_chat │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ └── ic_launcher.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── document ├── readme_screenshot_01.png ├── readme_screenshot_02.png ├── readme_screenshot_03.png ├── readme_screenshot_04.png └── readme_screenshot_tablet_01.png ├── lib ├── api │ └── openai_api.dart ├── bloc │ ├── app_bloc_observer.dart │ ├── blocs.dart │ ├── chat_bloc.dart │ ├── chat_event.dart │ ├── chat_state.dart │ ├── conversations_bloc.dart │ ├── conversations_event.dart │ └── conversations_state.dart ├── main.dart ├── models │ ├── chat.dart │ ├── conversation.dart │ └── models.dart ├── screens │ ├── chat_screen.dart │ ├── conversation_screen.dart │ ├── screens.dart │ ├── setting_screen.dart │ └── tablet_screen.dart ├── services │ ├── chat_service.dart │ ├── local_storage_service.dart │ └── token_service.dart ├── util │ ├── extend_http_client.dart │ ├── string_util.dart │ └── type_converter.dart └── widgets │ ├── chat_message_widget.dart │ ├── confirm_dialog.dart │ ├── conversation_edit_dialog.dart │ ├── conversation_list_widget.dart │ ├── empty_chat_widget.dart │ └── widgets.dart ├── macos ├── .gitignore ├── Flutter │ ├── Flutter-Debug.xcconfig │ ├── Flutter-Release.xcconfig │ └── GeneratedPluginRegistrant.swift ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── app_icon_1024.png │ │ ├── app_icon_128.png │ │ ├── app_icon_16.png │ │ ├── app_icon_256.png │ │ ├── app_icon_32.png │ │ ├── app_icon_512.png │ │ └── app_icon_64.png │ ├── Base.lproj │ └── MainMenu.xib │ ├── Configs │ ├── AppInfo.xcconfig │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── Warnings.xcconfig │ ├── DebugProfile.entitlements │ ├── Info.plist │ ├── MainFlutterWindow.swift │ └── Release.entitlements ├── pubspec.lock ├── pubspec.yaml └── test └── openai_api_test.dart /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.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 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 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 | -------------------------------------------------------------------------------- /.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. 5 | 6 | version: 7 | revision: 12cb4eb7a009f52b347b62ade7cb4854b926af72 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: 12cb4eb7a009f52b347b62ade7cb4854b926af72 17 | base_revision: 12cb4eb7a009f52b347b62ade7cb4854b926af72 18 | - platform: android 19 | create_revision: 12cb4eb7a009f52b347b62ade7cb4854b926af72 20 | base_revision: 12cb4eb7a009f52b347b62ade7cb4854b926af72 21 | - platform: macos 22 | create_revision: 12cb4eb7a009f52b347b62ade7cb4854b926af72 23 | base_revision: 12cb4eb7a009f52b347b62ade7cb4854b926af72 24 | 25 | # User provided section 26 | 27 | # List of Local paths (relative to this file) that should be 28 | # ignored by the migrate tool. 29 | # 30 | # Files that are not part of the templates will be ignored by default. 31 | unmanaged_files: 32 | - 'lib/main.dart' 33 | - 'ios/Runner.xcodeproj/project.pbxproj' 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 hahastudio 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutter Chat 2 | 3 | A new Flutter project which communicates with [OpenAI API](https://platform.openai.com/). 4 | 5 | ## Screenshots 6 | 7 | ![Screenshot 1](/document/readme_screenshot_01.png) 8 | ![Screenshot 2](/document/readme_screenshot_02.png) 9 | ![Screenshot 3](/document/readme_screenshot_03.png) 10 | ![Screenshot 4](/document/readme_screenshot_04.png) 11 | 12 | ![Screenshot tablet 1](/document/readme_screenshot_tablet_01.png) 13 | 14 | ## Features 15 | 16 | - Support [requesting organization](https://platform.openai.com/docs/api-reference/requesting-organization) 17 | - Support [system message](https://platform.openai.com/docs/guides/chat/introduction) 18 | - Support [streaming message](https://platform.openai.com/docs/api-reference/chat/create#chat/create-stream) like ChatGPT 19 | - Support to choose GPT models (gpt-3.5-turbo, gpt-3.5-turbo-16k, gpt-4, gpt-4-32k) 20 | - Support to limit the count of conversation history when sending 21 | - Support to show token usage in real time 22 | - Support customized API Host 23 | - Support tablet view 24 | 25 | ## How to use 26 | 27 | 1. Get [OpenAI API Key](https://platform.openai.com/docs/api-reference/authentication) 28 | 2. Tap setting button on top right corner to set API Key (required) and Organization (optional) 29 | 3. Add a new conversation 30 | 4. Chat with OpenAI 31 | 32 | ## Architecture 33 | 34 | It uses [Flutter framework](https://flutter.dev/), and uses [BLoC pattern](https://bloclibrary.dev/) to implement state management. 35 | 36 | ## How to build 37 | 38 | Get Flutter package: 39 | 40 | ``` 41 | flutter pub get 42 | ``` 43 | 44 | Build apk: 45 | 46 | ``` 47 | flutter build apk 48 | ``` 49 | 50 | NOTE: It may take quite long time to finish build. On my MacBook Pro, it takes about 1 hour to finish. 51 | 52 | ## References 53 | 54 | Mainly used Flutter packages: 55 | 56 | - [shared_preferences](https://pub.dev/packages/shared_preferences) to store app settings & conversations 57 | - [flutter_bloc](https://pub.dev/packages/flutter_bloc) for state management 58 | - [settings_ui](https://pub.dev/packages/settings_ui) for setting screen 59 | - [flutter_markdown](https://pub.dev/packages/flutter_markdown) to render messages in markdown format 60 | - Unofficial [tiktoken](https://pub.dev/packages/tiktoken) to calculate token usage 61 | 62 | ## License 63 | 64 | Distributed under the MIT License. See [LICENSE](LICENSE) for more information. 65 | -------------------------------------------------------------------------------- /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 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | curly_braces_in_flow_control_structures: false 26 | prefer_interpolation_to_compose_strings: false 27 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 28 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 29 | 30 | # Additional information about this file can be found at 31 | # https://dart.dev/guides/language/analysis-options 32 | -------------------------------------------------------------------------------- /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/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion flutter.compileSdkVersion 30 | ndkVersion flutter.ndkVersion 31 | 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_1_8 34 | targetCompatibility JavaVersion.VERSION_1_8 35 | } 36 | 37 | kotlinOptions { 38 | jvmTarget = '1.8' 39 | } 40 | 41 | sourceSets { 42 | main.java.srcDirs += 'src/main/kotlin' 43 | } 44 | 45 | defaultConfig { 46 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 47 | applicationId "com.hahastudio.flutterchat.flutter_chat" 48 | // You can update the following values to match your application needs. 49 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 50 | minSdkVersion flutter.minSdkVersion 51 | targetSdkVersion flutter.targetSdkVersion 52 | versionCode flutterVersionCode.toInteger() 53 | versionName flutterVersionName 54 | } 55 | 56 | buildTypes { 57 | release { 58 | // TODO: Add your own signing config for the release build. 59 | // Signing with the debug keys for now, so `flutter run --release` works. 60 | signingConfig signingConfigs.debug 61 | } 62 | } 63 | } 64 | 65 | flutter { 66 | source '../..' 67 | } 68 | 69 | dependencies { 70 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 71 | } 72 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 16 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/hahastudio/flutterchat/flutter_chat/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.hahastudio.flutterchat.flutter_chat 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.4.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 6 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /document/readme_screenshot_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/document/readme_screenshot_01.png -------------------------------------------------------------------------------- /document/readme_screenshot_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/document/readme_screenshot_02.png -------------------------------------------------------------------------------- /document/readme_screenshot_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/document/readme_screenshot_03.png -------------------------------------------------------------------------------- /document/readme_screenshot_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/document/readme_screenshot_04.png -------------------------------------------------------------------------------- /document/readme_screenshot_tablet_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/document/readme_screenshot_tablet_01.png -------------------------------------------------------------------------------- /lib/api/openai_api.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:core'; 4 | import 'dart:developer'; 5 | import 'package:http/http.dart' as http; 6 | 7 | import '../models/models.dart'; 8 | import '../services/local_storage_service.dart'; 9 | import '../util/extend_http_client.dart'; 10 | import '../util/string_util.dart'; 11 | 12 | class OpenAiApi { 13 | static const endPointHost = 'api.openai.com'; 14 | static const endPointPrefix = '/v1'; 15 | 16 | final SafeHttpClient httpClient; 17 | 18 | OpenAiApi(this.httpClient); 19 | 20 | Future chatCompletion(List messages) async { 21 | final uri = Uri.tryParse(stripTrailingSlash(LocalStorageService().apiHost) + '$endPointPrefix/chat/completions'); 22 | if (uri == null) 23 | throw Exception('API Host ${LocalStorageService().apiHost} is not valid'); 24 | 25 | var headers = { 26 | 'Content-Type': 'application/json' 27 | }; 28 | if (LocalStorageService().apiKey != '') { 29 | headers['Authorization'] = 'Bearer ${LocalStorageService().apiKey}'; 30 | } 31 | if (LocalStorageService().organization != '') { 32 | headers['OpenAI-Organization'] = LocalStorageService().organization; 33 | } 34 | 35 | var request = ChatRequest(messages); 36 | log('[OpenAiApi] ChatCompletion requested'); 37 | var response = await httpClient.post(uri, headers: headers, body: jsonEncode(request.toJson())); 38 | log('[OpenAiApi] ChatCompletion responded'); 39 | 40 | if (response.statusCode != 200) { 41 | String errorMessage = 'Error connecting OpenAI: '; 42 | try { 43 | var errorResponse = json.decode(utf8.decode(response.bodyBytes)); 44 | errorMessage += errorResponse['error']['message']; 45 | } catch (e) { 46 | errorMessage += 'Status code ${response.statusCode}'; 47 | } 48 | throw Exception(errorMessage); 49 | } 50 | 51 | var chatResponse = ChatResponse.fromJson(json.decode(utf8.decode(response.bodyBytes))); 52 | return chatResponse; 53 | } 54 | 55 | // The stream is like: 56 | // data: {"choices":[{"delta":{"role":"assistant"},"index":0,"finish_reason":null}],"id":"...","object":"chat.completion.chunk","created":1679123429,"model":"gpt-3.5-turbo-0301"} 57 | // 58 | // data: {"choices":[{"delta":{"content":"你"},"index":0,"finish_reason":null}],"id":"...","object":"chat.completion.chunk","created":1679123429,"model":"gpt-3.5-turbo-0301"} 59 | // 60 | // data: {"choices":[{"delta":{"content":"好"},"index":0,"finish_reason":null}],"id":"...","object":"chat.completion.chunk","created":1679123429,"model":"gpt-3.5-turbo-0301"} 61 | // 62 | // data: [DONE] 63 | Stream chatCompletionStream(List messages) { 64 | StreamController controller = StreamController(); 65 | 66 | final uri = Uri.tryParse(stripTrailingSlash(LocalStorageService().apiHost) + '$endPointPrefix/chat/completions'); 67 | if (uri == null) 68 | throw Exception('API Host ${LocalStorageService().apiHost} is not valid'); 69 | 70 | var headers = { 71 | 'Content-Type': 'application/json' 72 | }; 73 | if (LocalStorageService().apiKey != '') { 74 | headers['Authorization'] = 'Bearer ${LocalStorageService().apiKey}'; 75 | } 76 | if (LocalStorageService().organization != '') { 77 | headers['OpenAI-Organization'] = LocalStorageService().organization; 78 | } 79 | 80 | var requestBody = jsonEncode(ChatRequest(messages, model: LocalStorageService().model, stream: true).toJson()); 81 | http.Request request = http.Request('POST', uri); 82 | request.headers.addAll(headers); 83 | request.body = requestBody; 84 | 85 | log('[OpenAiApi] ChatCompletion Stream requested'); 86 | httpClient.send(request).then((response) { 87 | log('[OpenAiApi] ChatCompletion Stream response started'); 88 | response.stream.listen((value) { 89 | final String data = utf8.decode(value); 90 | // one response can contain multiple data line 91 | final List dataLines = data 92 | .split('\n') 93 | .where((element) => element.isNotEmpty) 94 | .toList(); 95 | 96 | for (String line in dataLines) { 97 | if (line.startsWith('data: ')) { 98 | final String data = line.substring(6); 99 | if (data.contains('[DONE]')) { 100 | log('[OpenAiApi] ChatCompletion Stream response finished'); 101 | return; 102 | } 103 | controller.add(ChatResponseStream.fromJson(jsonDecode(data))); 104 | continue; 105 | } 106 | // error handling 107 | final error = jsonDecode(data)['error']; 108 | if (error != null) { 109 | String errorMessage = ''; 110 | try { 111 | errorMessage += error['message']; 112 | } catch (e) { 113 | errorMessage += 'Status code ${response.statusCode}'; 114 | } 115 | controller.addError(Exception(errorMessage)); 116 | return; 117 | } 118 | } 119 | }, 120 | onDone: () { 121 | controller.close(); 122 | }, 123 | onError: (error, stackTrace) { 124 | controller.addError(error, stackTrace); 125 | }); // response.stream.listen 126 | }, 127 | onError: (error, stackTrace) { 128 | controller.addError(error, stackTrace); 129 | }); // httpClient.send(request).then 130 | 131 | return controller.stream; 132 | } 133 | } -------------------------------------------------------------------------------- /lib/bloc/app_bloc_observer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:bloc/bloc.dart'; 4 | 5 | class AppBlocObserver extends BlocObserver { 6 | const AppBlocObserver(); 7 | 8 | @override 9 | void onChange(BlocBase bloc, Change change) { 10 | super.onChange(bloc, change); 11 | log('onChange(${bloc.runtimeType}, $change)'); 12 | } 13 | 14 | @override 15 | void onError(BlocBase bloc, Object error, StackTrace stackTrace) { 16 | log('onError(${bloc.runtimeType}, $error, $stackTrace)'); 17 | super.onError(bloc, error, stackTrace); 18 | } 19 | } -------------------------------------------------------------------------------- /lib/bloc/blocs.dart: -------------------------------------------------------------------------------- 1 | export 'app_bloc_observer.dart'; 2 | export 'chat_bloc.dart'; 3 | export 'chat_event.dart'; 4 | export 'chat_state.dart'; 5 | export 'conversations_bloc.dart'; 6 | export 'conversations_event.dart'; 7 | export 'conversations_state.dart'; -------------------------------------------------------------------------------- /lib/bloc/chat_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_bloc/flutter_bloc.dart'; 2 | 3 | import '../models/models.dart'; 4 | import '../services/chat_service.dart'; 5 | import 'chat_event.dart'; 6 | import 'chat_state.dart'; 7 | 8 | class ChatBloc extends Bloc { 9 | ChatBloc({ 10 | required ChatService chatService, 11 | required Conversation initialConversation, 12 | }) : 13 | _chatService = chatService, 14 | super( 15 | ChatState( 16 | initialConversation: initialConversation, 17 | id: initialConversation.id, 18 | title: initialConversation.title, 19 | lastUpdated: initialConversation.lastUpdated 20 | ), 21 | ) 22 | { 23 | on(_onLastUpdatedChanged); 24 | on(_onSubmitted); 25 | on(_onStreamStarted); 26 | on(_onStreaming); 27 | on(_onStreamEnded); 28 | } 29 | 30 | final ChatService _chatService; 31 | 32 | void _onLastUpdatedChanged( 33 | ChatLastUpdatedChanged event, 34 | Emitter emit, 35 | ) { 36 | emit(state.copyWith(initialConversation: event.conversation, lastUpdated: event.lastUpdated)); 37 | } 38 | 39 | Future _onSubmitted( 40 | ChatSubmitted event, 41 | Emitter emit, 42 | ) async { 43 | emit(state.copyWith(initialConversation: event.conversation, status: ChatStatus.loading)); 44 | 45 | try { 46 | var newConversation = await _chatService.getResponseFromServer(event.conversation); 47 | emit(state.copyWith( 48 | initialConversation: newConversation, 49 | status: newConversation.messages.last.isError ? ChatStatus.failure : ChatStatus.success 50 | )); 51 | } catch (e) { 52 | emit(state.copyWith(initialConversation: event.conversation, status: ChatStatus.failure)); 53 | } 54 | } 55 | 56 | void _onStreamStarted( 57 | ChatStreamStarted event, 58 | Emitter emit, 59 | ) { 60 | emit(state.copyWith(initialConversation: event.conversation, status: ChatStatus.loading)); 61 | } 62 | 63 | void _onStreaming( 64 | ChatStreaming event, 65 | Emitter emit, 66 | ) { 67 | emit(state.copyWith( 68 | initialConversation: event.conversation, 69 | lastUpdated: event.lastUpdated, 70 | status: event.conversation.error.isNotEmpty ? ChatStatus.failure : ChatStatus.loading 71 | )); 72 | } 73 | 74 | void _onStreamEnded( 75 | ChatStreamEnded event, 76 | Emitter emit, 77 | ) { 78 | emit(state.copyWith( 79 | initialConversation: event.conversation, 80 | status: event.conversation.error.isNotEmpty ? ChatStatus.failure : ChatStatus.success 81 | )); 82 | } 83 | } -------------------------------------------------------------------------------- /lib/bloc/chat_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import '../models/models.dart'; 4 | 5 | abstract class ChatEvent extends Equatable { 6 | const ChatEvent(); 7 | 8 | @override 9 | List get props => []; 10 | } 11 | 12 | class ChatLastUpdatedChanged extends ChatEvent { 13 | const ChatLastUpdatedChanged(this.conversation, this.lastUpdated); 14 | 15 | final DateTime lastUpdated; 16 | final Conversation conversation; 17 | 18 | @override 19 | List get props => [lastUpdated]; 20 | } 21 | 22 | class ChatSubmitted extends ChatEvent { 23 | const ChatSubmitted(this.conversation); 24 | 25 | final Conversation conversation; 26 | } 27 | 28 | class ChatStreamStarted extends ChatEvent { 29 | const ChatStreamStarted(this.conversation); 30 | 31 | final Conversation conversation; 32 | } 33 | 34 | class ChatStreaming extends ChatEvent { 35 | const ChatStreaming(this.conversation, this.lastUpdated); 36 | 37 | final DateTime lastUpdated; 38 | final Conversation conversation; 39 | 40 | @override 41 | List get props => [lastUpdated]; 42 | } 43 | 44 | class ChatStreamEnded extends ChatEvent { 45 | const ChatStreamEnded(this.conversation); 46 | 47 | final Conversation conversation; 48 | } -------------------------------------------------------------------------------- /lib/bloc/chat_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import '../models/models.dart'; 4 | 5 | enum ChatStatus { initial, loading, success, failure } 6 | 7 | class ChatState extends Equatable { 8 | const ChatState({ 9 | this.status = ChatStatus.initial, 10 | required this.initialConversation, 11 | required this.id, 12 | required this.title, 13 | required this.lastUpdated 14 | }); 15 | 16 | final ChatStatus status; 17 | final Conversation initialConversation; 18 | final String id; 19 | final String title; 20 | final DateTime lastUpdated; 21 | 22 | ChatState copyWith({ 23 | ChatStatus? status, 24 | Conversation? initialConversation, 25 | String? id, 26 | String? title, 27 | DateTime? lastUpdated 28 | }) { 29 | return ChatState( 30 | status: status ?? this.status, 31 | initialConversation: initialConversation ?? this.initialConversation, 32 | id: id ?? this.id, 33 | title: title ?? this.title, 34 | lastUpdated: lastUpdated ?? this.lastUpdated 35 | ); 36 | } 37 | 38 | @override 39 | List get props => [status, id, title, lastUpdated]; 40 | } -------------------------------------------------------------------------------- /lib/bloc/conversations_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | 3 | import '../services/chat_service.dart'; 4 | import 'conversations_state.dart'; 5 | import 'conversations_event.dart'; 6 | 7 | class ConversationsBloc extends Bloc { 8 | ConversationsBloc({ 9 | required ChatService chatService 10 | }) : 11 | _chatService = chatService, 12 | super(const ConversationsState()) 13 | { 14 | on(_onRequested); 15 | on(_onDeleted); 16 | } 17 | 18 | final ChatService _chatService; 19 | 20 | Future _onRequested( 21 | ConversationsRequested event, 22 | Emitter emit, 23 | ) async { 24 | emit(state.copyWith(status: ConversationsStatus.loading)); 25 | emit(state.copyWith(conversations: _chatService.getConversationList(), status: ConversationsStatus.success)); 26 | } 27 | 28 | Future _onDeleted( 29 | ConversationDeleted event, 30 | Emitter emit, 31 | ) async { 32 | emit(state.copyWith(status: ConversationsStatus.loading)); 33 | await _chatService.removeConversationById(event.conversationIndex.id); 34 | emit(state.copyWith(conversations: _chatService.getConversationList(), status: ConversationsStatus.success)); 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /lib/bloc/conversations_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import '../models/models.dart'; 4 | 5 | abstract class ConversationsEvent extends Equatable { 6 | const ConversationsEvent(); 7 | 8 | @override 9 | List get props => []; 10 | } 11 | 12 | class ConversationsRequested extends ConversationsEvent { 13 | const ConversationsRequested(); 14 | } 15 | 16 | class ConversationDeleted extends ConversationsEvent { 17 | const ConversationDeleted(this.conversationIndex); 18 | 19 | final ConversationIndex conversationIndex; 20 | 21 | @override 22 | List get props => [conversationIndex]; 23 | } 24 | -------------------------------------------------------------------------------- /lib/bloc/conversations_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import '../models/models.dart'; 4 | 5 | enum ConversationsStatus { initial, loading, success, failure } 6 | 7 | class ConversationsState extends Equatable { 8 | const ConversationsState({ 9 | this.status = ConversationsStatus.initial, 10 | this.conversations = const [], 11 | }); 12 | 13 | final ConversationsStatus status; 14 | final List conversations; 15 | 16 | ConversationsState copyWith({ 17 | ConversationsStatus? status, 18 | List? conversations 19 | }) { 20 | return ConversationsState( 21 | status: status ?? this.status, 22 | conversations: conversations ?? this.conversations 23 | ); 24 | } 25 | 26 | @override 27 | List get props => [status, conversations]; 28 | } -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:developer'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_bloc/flutter_bloc.dart'; 6 | import 'package:http/http.dart' as http; 7 | 8 | import 'api/openai_api.dart'; 9 | import 'bloc/blocs.dart'; 10 | import 'screens/screens.dart'; 11 | import 'services/chat_service.dart'; 12 | import 'services/local_storage_service.dart'; 13 | import 'util/extend_http_client.dart'; 14 | 15 | void main() async { 16 | WidgetsFlutterBinding.ensureInitialized(); 17 | await LocalStorageService().init(); 18 | Bloc.observer = const AppBlocObserver(); 19 | final openAiApi = OpenAiApi(SafeHttpClient(http.Client())); 20 | final chatService = ChatService(apiServer: openAiApi); 21 | 22 | // TODO: init token service in background to speed up ChatScreen on the first load 23 | 24 | runZonedGuarded( 25 | () => runApp(App(chatService: chatService)), 26 | (error, stackTrace) => log(error.toString(), stackTrace: stackTrace), 27 | ); 28 | } 29 | 30 | class App extends StatelessWidget { 31 | const App({super.key, required this.chatService}); 32 | 33 | final ChatService chatService; 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | return RepositoryProvider.value( 38 | value: chatService, 39 | child: BlocProvider( 40 | create: (context) => ConversationsBloc( 41 | chatService: context.read(), 42 | )..add(const ConversationsRequested()), 43 | child: MaterialApp( 44 | theme: ThemeData.dark(useMaterial3: true), 45 | home: const ConversationScreenPage(), 46 | ) 47 | ) 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/models/chat.dart: -------------------------------------------------------------------------------- 1 | import '../util/type_converter.dart'; 2 | 3 | /// Document: https://platform.openai.com/docs/api-reference/chat 4 | 5 | class ChatMessage { 6 | /// either “system”, “user”, or “assistant” 7 | final String role; 8 | /// the content of the message 9 | final String content; 10 | 11 | ChatMessage(this.role, this.content); 12 | 13 | static ChatMessage fromJson(Map json) => 14 | ChatMessage(json['role'], json['content']); 15 | 16 | static List fromListJson(List json) { 17 | final messages = []; 18 | for (final item in json) { 19 | messages.add(ChatMessage.fromJson(item)); 20 | } 21 | return messages; 22 | } 23 | 24 | Map toJson() => { 25 | 'role': role, 26 | 'content': content 27 | }; 28 | } 29 | 30 | class ChatRequest { 31 | /// ID of the model to use. Currently, only gpt-3.5-turbo and gpt-3.5-turbo-0301 are supported. 32 | final String model; 33 | static const String defaultModel = 'gpt-3.5-turbo'; 34 | /// The messages to generate chat completions for 35 | final List messages; 36 | /// 37 | final bool stream; 38 | 39 | ChatRequest(this.messages, {this.model = defaultModel, this.stream = false}); 40 | 41 | static ChatRequest fromJson(Map json) => 42 | ChatRequest(ChatMessage.fromListJson(json['messages'])); 43 | 44 | Map toJson() => { 45 | 'model': model, 46 | 'messages': messages.map((m) => m.toJson()).toList(), 47 | 'stream': stream 48 | }; 49 | } 50 | 51 | class ChatResponseChoice { 52 | final int index; 53 | final ChatMessage message; 54 | final String finishReason; 55 | 56 | ChatResponseChoice(this.index, this.message, this.finishReason); 57 | 58 | static ChatResponseChoice fromJson(Map json) => 59 | ChatResponseChoice(json['index'], 60 | ChatMessage.fromJson(json['message']), 61 | json['finish_reason'] ?? '' 62 | ); 63 | 64 | static List fromListJson(List json) { 65 | final choices = []; 66 | for (final item in json) { 67 | choices.add(ChatResponseChoice.fromJson(item)); 68 | } 69 | return choices; 70 | } 71 | 72 | Map toJson() => { 73 | 'index': index, 74 | 'message': message.toJson(), 75 | 'finish_reason': finishReason 76 | }; 77 | } 78 | 79 | class ChatResponseUsage { 80 | final int promptTokens = 0; 81 | final int completionTokens = 0; 82 | final int totalTokens = 0; 83 | 84 | ChatResponseUsage(promptTokens, completionTokens, totalTokens); 85 | 86 | static ChatResponseUsage fromJson(Map json) => 87 | ChatResponseUsage(json['prompt_tokens'], 88 | json['completion_tokens'], 89 | json['total_tokens'] 90 | ); 91 | } 92 | 93 | class ChatResponse { 94 | final String id; 95 | final String object; 96 | final DateTime created; 97 | final List choices; 98 | final ChatResponseUsage usage; 99 | 100 | ChatResponse(this.id, this.object, this.created, this.choices, this.usage); 101 | 102 | static ChatResponse fromJson(Map json) => 103 | ChatResponse(json['id'], 104 | json['object'], 105 | DateTime.fromMillisecondsSinceEpoch(doubleToInt(json['created']) * 1000), 106 | ChatResponseChoice.fromListJson(json['choices']), 107 | ChatResponseUsage.fromJson(json['usage']) 108 | ); 109 | } 110 | 111 | class ChatResponseDelta { 112 | String role; 113 | String content; 114 | 115 | ChatResponseDelta(this.role, this.content); 116 | 117 | static ChatResponseDelta fromJson(Map json) => 118 | ChatResponseDelta( 119 | json['role'] ?? '', 120 | json['content'] ?? '' 121 | ); 122 | } 123 | 124 | class ChatResponseChoiceStream { 125 | final int index; 126 | final ChatResponseDelta delta; 127 | final String finishReason; 128 | 129 | ChatResponseChoiceStream(this.index, this.delta, this.finishReason); 130 | 131 | static ChatResponseChoiceStream fromJson(Map json) => 132 | ChatResponseChoiceStream(json['index'], 133 | ChatResponseDelta.fromJson(json['delta']), 134 | json['finish_reason'] ?? '' 135 | ); 136 | 137 | static List fromListJson(List json) { 138 | final choices = []; 139 | for (final item in json) { 140 | choices.add(ChatResponseChoiceStream.fromJson(item)); 141 | } 142 | return choices; 143 | } 144 | } 145 | 146 | class ChatResponseStream { 147 | final String id; 148 | final String object; 149 | final DateTime created; 150 | final List choices; 151 | 152 | ChatResponseStream(this.id, this.object, this.created, this.choices); 153 | 154 | static ChatResponseStream fromJson(Map json) => 155 | ChatResponseStream(json['id'], 156 | json['object'], 157 | DateTime.fromMillisecondsSinceEpoch(doubleToInt(json['created']) * 1000), 158 | ChatResponseChoiceStream.fromListJson(json['choices']), 159 | ); 160 | } -------------------------------------------------------------------------------- /lib/models/conversation.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:uuid/uuid.dart'; 3 | 4 | import 'chat.dart'; 5 | import '../util/type_converter.dart'; 6 | 7 | class Conversation { 8 | final String id; 9 | String title; 10 | DateTime lastUpdated; 11 | String systemMessage; 12 | List messages; 13 | String error; 14 | 15 | Conversation( 16 | this.id, 17 | this.title, 18 | this.systemMessage, 19 | this.messages, 20 | { 21 | DateTime? lastUpdated, 22 | this.error = '' 23 | }) : lastUpdated = lastUpdated ?? DateTime.now(); 24 | 25 | static Conversation create() => 26 | Conversation( 27 | const Uuid().v4(), 28 | '', 29 | '', 30 | [] 31 | ); 32 | 33 | static Conversation fromJson(Map json) => 34 | Conversation( 35 | json['id'], 36 | json['title'], 37 | json['system_message'], 38 | ConversationMessage.fromListJson(json['messages']), 39 | lastUpdated: DateTime.fromMillisecondsSinceEpoch(doubleToInt(json['last_updated']) * 1000), 40 | error: json['error'] 41 | ); 42 | 43 | Map toJson() => { 44 | 'id': id, 45 | 'title': title, 46 | 'last_updated': lastUpdated.millisecondsSinceEpoch / 1000, 47 | 'system_message': systemMessage, 48 | 'messages': messages.map((m) => m.toJson()).toList(), 49 | 'error': error 50 | }; 51 | } 52 | 53 | class ConversationMessage { 54 | /// either “system”, “user”, or “assistant” 55 | final String role; 56 | String content; 57 | bool isError; 58 | 59 | ConversationMessage(this.role, this.content, { this.isError = false }); 60 | 61 | static ConversationMessage fromChatMessage(ChatMessage message) => 62 | ConversationMessage(message.role, message.content); 63 | 64 | ChatMessage toChatMessage() => 65 | ChatMessage(role, content); 66 | 67 | static ConversationMessage fromJson(Map json) => 68 | ConversationMessage(json['role'], json['content'], isError: json['is_error']); 69 | 70 | static List fromListJson(List json) { 71 | final messages = []; 72 | for (final item in json) { 73 | messages.add(ConversationMessage.fromJson(item)); 74 | } 75 | return messages; 76 | } 77 | 78 | Map toJson() => { 79 | 'role': role, 80 | 'content': content, 81 | 'is_error': isError 82 | }; 83 | } 84 | 85 | class ConversationIndex extends Equatable { 86 | final String id; 87 | final String title; 88 | final DateTime lastUpdated; 89 | 90 | ConversationIndex( 91 | this.id, 92 | this.title, 93 | { 94 | DateTime? lastUpdated 95 | }) : lastUpdated = lastUpdated ?? DateTime.now(); 96 | 97 | @override 98 | List get props => [id, title]; 99 | 100 | static ConversationIndex fromConversation(Conversation c) => 101 | ConversationIndex(c.id, c.title, lastUpdated: c.lastUpdated); 102 | 103 | static ConversationIndex fromJson(Map json) => 104 | ConversationIndex( 105 | json['id'], 106 | json['title'], 107 | lastUpdated: DateTime.fromMillisecondsSinceEpoch(doubleToInt(json['last_updated']) * 1000), 108 | ); 109 | 110 | static List fromListJson(List json) { 111 | final indices = []; 112 | for (final item in json) { 113 | indices.add(ConversationIndex.fromJson(item)); 114 | } 115 | return indices; 116 | } 117 | 118 | Map toJson() => { 119 | 'id': id, 120 | 'title': title, 121 | 'last_updated': lastUpdated.millisecondsSinceEpoch / 1000, 122 | }; 123 | } -------------------------------------------------------------------------------- /lib/models/models.dart: -------------------------------------------------------------------------------- 1 | export 'chat.dart'; 2 | export 'conversation.dart'; -------------------------------------------------------------------------------- /lib/screens/chat_screen.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_bloc/flutter_bloc.dart'; 5 | 6 | import '../bloc/blocs.dart'; 7 | import '../models/models.dart'; 8 | import '../services/chat_service.dart'; 9 | import '../services/local_storage_service.dart'; 10 | import '../services/token_service.dart'; 11 | import '../widgets/widgets.dart'; 12 | import 'screens.dart'; 13 | 14 | class ChatScreenPage extends StatelessWidget { 15 | const ChatScreenPage({super.key}); 16 | 17 | static Route route(Conversation initialConversation) { 18 | return PageRouteBuilder( 19 | pageBuilder: (context, animation, secondaryAnimation) => BlocProvider( 20 | create: (context) => ChatBloc( 21 | chatService: context.read(), 22 | initialConversation: initialConversation, 23 | ), 24 | child: TabletScreenPage( 25 | sidebar: ConversationScreen(selectedConversation: initialConversation), 26 | body: const ChatScreen() 27 | ), 28 | ), 29 | transitionDuration: Duration.zero, 30 | ); 31 | } 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | return BlocListener( 36 | listenWhen: (previous, current) => 37 | previous.status != current.status && 38 | current.status == ChatStatus.success, 39 | listener: (context, state) => Navigator.of(context).pop(), 40 | child: const ChatScreen(), 41 | ); 42 | } 43 | } 44 | 45 | class ChatScreen extends StatefulWidget { 46 | const ChatScreen({super.key}); 47 | 48 | @override 49 | State createState() => _ChatScreenState(); 50 | } 51 | 52 | class _ChatScreenState extends State { 53 | 54 | final GlobalKey scaffoldMessengerKey = GlobalKey(); 55 | late ScrollController _scrollController; 56 | late TextEditingController _textEditingController; 57 | late FocusNode _focusNode; 58 | bool _showSystemMessage = false; 59 | 60 | @override 61 | void initState() { 62 | _scrollController = ScrollController(); 63 | _textEditingController = TextEditingController(); 64 | _focusNode = FocusNode(); 65 | super.initState(); 66 | } 67 | 68 | @override 69 | void dispose() { 70 | _scrollController.dispose(); 71 | _textEditingController.dispose(); 72 | _focusNode.dispose(); 73 | super.dispose(); 74 | } 75 | 76 | Future showConversationDialog(BuildContext context, bool isEdit, Conversation conversation) => showDialog( 77 | context: context, 78 | builder: (context) { 79 | return ConversationEditDialog(conversation: conversation, isEdit: isEdit); 80 | } 81 | ); 82 | 83 | Future showClearConfirmDialog(BuildContext context) => showDialog( 84 | context: context, 85 | builder: (BuildContext context) { 86 | return const ConfirmDialog( 87 | title: 'Clear conversation', 88 | content: 'Would you like to clear conversation history?', 89 | ); 90 | }, 91 | ); 92 | 93 | void handleSend(BuildContext context, Conversation conversation) { 94 | if (TokenService.getToken(conversation.systemMessage) + TokenService.getToken(_textEditingController.text) >= TokenService.getTokenLimit()) 95 | return; 96 | var chatService = context.read(); 97 | var newMessage = ConversationMessage('user', _textEditingController.text); 98 | _textEditingController.text = ''; 99 | if (conversation.messages.isNotEmpty && conversation.messages.last.role == 'user') { 100 | conversation.messages.last = newMessage; 101 | } else { 102 | conversation.messages.add(newMessage); 103 | } 104 | BlocProvider.of(context).add(ChatStreamStarted(conversation)); 105 | chatService.getResponseStreamFromServer(conversation).listen((conversation) { 106 | BlocProvider.of(context).add(ChatStreaming(conversation, conversation.lastUpdated)); 107 | _scrollController.animateTo( 108 | _scrollController.position.maxScrollExtent - 10, 109 | duration: const Duration(milliseconds: 200), 110 | curve: Curves.fastOutSlowIn 111 | ); 112 | }, 113 | onDone: () { 114 | BlocProvider.of(context).add(ChatStreamEnded(conversation)); 115 | BlocProvider.of(context).add(const ConversationsRequested()); 116 | }); 117 | } 118 | 119 | void handleRefresh(BuildContext context, Conversation conversation) { 120 | var chatService = context.read(); 121 | if (conversation.messages.last.role == 'assistant') { 122 | conversation.messages.removeLast(); 123 | } 124 | BlocProvider.of(context).add(ChatStreamStarted(conversation)); 125 | chatService.getResponseStreamFromServer(conversation).listen((conversation) { 126 | BlocProvider.of(context).add(ChatStreaming(conversation, conversation.lastUpdated)); 127 | _scrollController.animateTo( 128 | _scrollController.position.maxScrollExtent - 10, 129 | duration: const Duration(milliseconds: 200), 130 | curve: Curves.fastOutSlowIn 131 | ); 132 | }, 133 | onDone: () { 134 | BlocProvider.of(context).add(ChatStreamEnded(conversation)); 135 | BlocProvider.of(context).add(const ConversationsRequested()); 136 | }); 137 | } 138 | 139 | @override 140 | Widget build(BuildContext context) { 141 | final state = context.watch().state; 142 | var conversation = state.initialConversation; 143 | var chatService = context.read(); 144 | var chatBloc = BlocProvider.of(context); 145 | var conversationsBloc = BlocProvider.of(context); 146 | var isMarkdown = LocalStorageService().renderMode == 'markdown'; 147 | 148 | if (state.status == ChatStatus.failure) { 149 | WidgetsBinding.instance.addPostFrameCallback((_) { 150 | scaffoldMessengerKey.currentState?.showSnackBar( 151 | SnackBar( 152 | content: Text(conversation.error), 153 | action: SnackBarAction( 154 | label: 'Resend', 155 | onPressed: () { 156 | BlocProvider.of(context).add(ChatSubmitted(conversation)); 157 | }, 158 | ), 159 | ), 160 | ); 161 | }); 162 | } 163 | return ScaffoldMessenger( 164 | key: scaffoldMessengerKey, 165 | child: Scaffold( 166 | appBar: AppBar( 167 | title: Text(conversation.title, style: const TextStyle(overflow: TextOverflow.ellipsis)), 168 | actions: [ 169 | IconButton( 170 | icon: const Icon(Icons.info), 171 | onPressed: () { 172 | setState(() { 173 | _showSystemMessage = !_showSystemMessage; 174 | }); 175 | }, 176 | ), 177 | PopupMenuButton( 178 | icon: const Icon(Icons.more_vert), 179 | itemBuilder: (context) { 180 | return const [ 181 | PopupMenuItem( 182 | value: 'edit', 183 | child: Text('Edit'), 184 | ), 185 | PopupMenuItem( 186 | value: 'clear', 187 | child: Text('Clear conversation'), 188 | ), 189 | ]; 190 | }, 191 | onSelected: (value) async { 192 | switch (value) { 193 | case 'edit': 194 | var newConversation = await showConversationDialog(context, true, conversation); 195 | if (newConversation != null) { 196 | conversation.lastUpdated = DateTime.now(); 197 | await chatService.updateConversation(newConversation); 198 | chatBloc.add(ChatLastUpdatedChanged(conversation, conversation.lastUpdated)); 199 | conversationsBloc.add(const ConversationsRequested()); 200 | } 201 | break; 202 | case 'clear': 203 | var result = await showClearConfirmDialog(context); 204 | if (result == true) { 205 | conversation.messages = []; 206 | conversation.lastUpdated = DateTime.now(); 207 | await chatService.updateConversation(conversation); 208 | chatBloc.add(ChatLastUpdatedChanged(conversation, conversation.lastUpdated)); 209 | conversationsBloc.add(const ConversationsRequested()); 210 | } 211 | break; 212 | default: 213 | break; 214 | } 215 | }, 216 | ), 217 | ] 218 | ), 219 | body: SafeArea( 220 | child: Column( 221 | children: [ 222 | // system message 223 | if(_showSystemMessage) Padding( 224 | padding: const EdgeInsets.all(10), 225 | child: Row( 226 | children: [ 227 | Expanded( 228 | child: SelectableText(conversation.systemMessage, maxLines: 5) 229 | ) 230 | ], 231 | ) 232 | ), 233 | // loading indicator 234 | if (state.status == ChatStatus.loading) 235 | const LinearProgressIndicator(), 236 | // chat messages 237 | Expanded( 238 | child: ScrollConfiguration( 239 | behavior: const ScrollBehavior(), 240 | child: ListView.builder( 241 | controller: _scrollController, 242 | physics: (state.status == ChatStatus.loading) ? const NeverScrollableScrollPhysics() : null, 243 | itemCount: (state.status == ChatStatus.loading) ? conversation.messages.length + 1 : conversation.messages.length, 244 | itemBuilder: (context, index) { 245 | if ((state.status == ChatStatus.loading) && (index == conversation.messages.length)) 246 | return const SizedBox(height: 60); 247 | else 248 | return ChatMessageWidget(message: conversation.messages[index], isMarkdown: isMarkdown); 249 | }, 250 | ) 251 | ) 252 | ), 253 | // status bar 254 | ValueListenableBuilder( 255 | valueListenable: _textEditingController, 256 | builder: (context, value, child) { 257 | return SizedBox( 258 | height: 24, 259 | child: Container( 260 | padding: const EdgeInsets.only(left: 16), 261 | child: Row( 262 | children: [ 263 | Row( 264 | children: [ 265 | Icon(Icons.history, size: 16, color: Theme.of(context).colorScheme.primary), 266 | const SizedBox(width: 8), 267 | Text('${min(TokenService.getEffectiveMessages(conversation, value.text).length, LocalStorageService().historyCount)}/${LocalStorageService().historyCount}', 268 | style: const TextStyle(fontSize: 12) 269 | ) 270 | ], 271 | ), 272 | const SizedBox(width: 20), 273 | Row( 274 | children: [ 275 | Icon(Icons.translate, size: 16, color: Theme.of(context).colorScheme.primary), 276 | const SizedBox(width: 8), 277 | Text('System: ${TokenService.getToken(conversation.systemMessage)}', 278 | style: TextStyle( 279 | fontSize: 12, 280 | color: TokenService.getToken(conversation.systemMessage) >= TokenService.getTokenLimit() ? 281 | Theme.of(context).colorScheme.error : 282 | null 283 | ) 284 | ), 285 | const SizedBox(width: 8), 286 | Text('Input: ${TokenService.getToken(value.text)}', 287 | style: TextStyle( 288 | fontSize: 12, 289 | color: TokenService.getToken(conversation.systemMessage) + TokenService.getToken(value.text) >= TokenService.getTokenLimit() ? 290 | Theme.of(context).colorScheme.error : 291 | null 292 | ) 293 | ), 294 | const SizedBox(width: 8), 295 | Text('History: ${TokenService.getEffectiveMessagesToken(conversation, value.text)}', 296 | style: const TextStyle(fontSize: 12) 297 | ), 298 | ], 299 | ) 300 | ], 301 | ), 302 | ), 303 | ); 304 | } 305 | ), 306 | // chat input 307 | Container( 308 | padding: const EdgeInsets.only(left: 12, top: 4, bottom: 8), 309 | alignment: Alignment.centerRight, 310 | child: Row( 311 | children: [ 312 | Expanded( 313 | child: Container( 314 | decoration: BoxDecoration( 315 | color: Color.lerp(Theme.of(context).colorScheme.background, Colors.white, 0.1), 316 | borderRadius: BorderRadius.circular(8), 317 | ), 318 | padding: const EdgeInsets.only(left: 8), 319 | child: Row( 320 | children: [ 321 | Expanded( 322 | child: TextField( 323 | decoration: const InputDecoration( 324 | hintText: 'Send a message...', 325 | border: InputBorder.none 326 | ), 327 | controller: _textEditingController, 328 | focusNode: _focusNode, 329 | minLines: 1, 330 | maxLines: 3, 331 | onSubmitted: (value) async { }, 332 | ), 333 | ), 334 | ValueListenableBuilder( 335 | valueListenable: _textEditingController, 336 | builder: (context, value, child) { 337 | return IconButton( 338 | icon: const Icon(Icons.send), 339 | color: TokenService.getToken(conversation.systemMessage) + TokenService.getToken(value.text) >= TokenService.getTokenLimit() ? 340 | Theme.of(context).colorScheme.error : 341 | null, 342 | onPressed: (state.status == ChatStatus.loading) || (value.text.isEmpty || value.text.trim().isEmpty) 343 | ? null 344 | : () => handleSend(context, conversation) 345 | ); 346 | } 347 | ), 348 | ], 349 | ), 350 | ), 351 | ), 352 | IconButton( 353 | icon: const Icon(Icons.refresh), 354 | onPressed: (state.status == ChatStatus.loading) || (conversation.messages.isEmpty) 355 | ? null 356 | : () => handleRefresh(context, conversation) 357 | ) 358 | ], 359 | ) 360 | ) 361 | ] 362 | ) 363 | ) 364 | ), 365 | ); 366 | } 367 | 368 | } -------------------------------------------------------------------------------- /lib/screens/conversation_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | import '../bloc/blocs.dart'; 5 | import '../models/models.dart'; 6 | import '../services/chat_service.dart'; 7 | import '../widgets/widgets.dart'; 8 | import 'screens.dart'; 9 | 10 | class ConversationScreenPage extends StatelessWidget { 11 | const ConversationScreenPage({super.key}); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return const TabletScreenPage( 16 | sidebar: ConversationScreen(), 17 | body: EmptyChatWidget(), 18 | mainView: TabletMainView.sidebar, 19 | ); 20 | } 21 | } 22 | 23 | class ConversationScreen extends StatelessWidget { 24 | final Conversation? selectedConversation; 25 | 26 | const ConversationScreen({super.key, this.selectedConversation}); 27 | 28 | Future showConversationDialog(BuildContext context, bool isEdit, Conversation conversation) => showDialog( 29 | context: context, 30 | builder: (context) { 31 | return ConversationEditDialog(conversation: conversation, isEdit: isEdit); 32 | } 33 | ); 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | var chatService = context.read(); 38 | var bloc = BlocProvider.of(context); 39 | 40 | return Scaffold( 41 | appBar: AppBar( 42 | title: const Text('Conversations'), 43 | automaticallyImplyLeading: false, 44 | actions: [ 45 | IconButton( 46 | icon: const Icon(Icons.settings), 47 | onPressed: () { 48 | Navigator.of(context).push(MaterialPageRoute( 49 | builder: (_) => const SettingsScreen() 50 | )); 51 | }, 52 | ) 53 | ] 54 | ), 55 | body: SafeArea( 56 | child: Padding( 57 | padding: const EdgeInsets.all(10), 58 | child: Column( 59 | children: [ 60 | ConversationListWidget(selectedConversation: selectedConversation) 61 | ], 62 | ), 63 | ) 64 | ), 65 | floatingActionButton: FloatingActionButton( 66 | onPressed: () async { 67 | var newConversation = await showConversationDialog(context, false, Conversation.create()); 68 | if (newConversation != null) { 69 | await chatService.updateConversation(newConversation); 70 | var savedConversation = chatService.getConversationById(newConversation.id)!; 71 | if (context.mounted) { 72 | if (Navigator.of(context).canPop()) { 73 | Navigator.of(context).pushReplacement(ChatScreenPage.route(savedConversation)); 74 | } else { 75 | Navigator.of(context).push(ChatScreenPage.route(savedConversation)); 76 | } 77 | } 78 | bloc.add(const ConversationsRequested()); 79 | } 80 | }, 81 | child: const Icon(Icons.add), 82 | ), 83 | ); 84 | } 85 | } -------------------------------------------------------------------------------- /lib/screens/screens.dart: -------------------------------------------------------------------------------- 1 | export 'chat_screen.dart'; 2 | export 'conversation_screen.dart'; 3 | export 'setting_screen.dart'; 4 | export 'tablet_screen.dart'; -------------------------------------------------------------------------------- /lib/screens/setting_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:settings_ui/settings_ui.dart'; 3 | import 'package:url_launcher/url_launcher.dart'; 4 | 5 | import '../services/local_storage_service.dart'; 6 | import '../util/string_util.dart'; 7 | 8 | class SettingsScreen extends StatefulWidget { 9 | const SettingsScreen({super.key}); 10 | 11 | @override 12 | State createState() => _SettingsScreenState(); 13 | } 14 | 15 | class _SettingsScreenState extends State { 16 | 17 | String apiKey = LocalStorageService().apiKey; 18 | String organization = LocalStorageService().organization; 19 | String apiHost = LocalStorageService().apiHost; 20 | String model = LocalStorageService().model; 21 | int historyCount = LocalStorageService().historyCount; 22 | String renderMode = LocalStorageService().renderMode; 23 | 24 | final _textFieldController = TextEditingController(); 25 | 26 | Future openStringDialog (BuildContext context, String title, String hintText) => showDialog( 27 | context: context, 28 | builder: (context) { 29 | return AlertDialog( 30 | title: Text(title), 31 | content: TextField( 32 | controller: _textFieldController, 33 | decoration: InputDecoration(hintText: hintText), 34 | ), 35 | actions: [ 36 | TextButton( 37 | child: const Text('Cancel'), 38 | onPressed: () => Navigator.pop(context), 39 | ), 40 | ElevatedButton( 41 | child: const Text('OK'), 42 | onPressed: () => Navigator.pop(context, _textFieldController.text), 43 | ), 44 | ], 45 | ); 46 | } 47 | ); 48 | 49 | String obscureApiKey(String apiKey) { 50 | if (apiKey.length < 7) 51 | return 'Invalid API Key'; 52 | if (apiKey.substring(0, 3) != 'sk-') 53 | return 'Invalid API Key'; 54 | return 'sk-...' + LocalStorageService().apiKey.substring( 55 | LocalStorageService().apiKey.length - 4, LocalStorageService().apiKey.length 56 | ); 57 | } 58 | 59 | String getRenderModeDescription(String renderMode) { 60 | if (renderMode == 'markdown') 61 | return 'Markdown'; 62 | if (renderMode == 'text') 63 | return 'Plain Text'; 64 | return 'Unknown'; 65 | } 66 | 67 | @override 68 | Widget build(BuildContext context) { 69 | return Scaffold( 70 | appBar: AppBar(title: const Text('Settings')), 71 | body: SettingsList( 72 | sections: [ 73 | SettingsSection( 74 | title: const Text('Authentication'), 75 | tiles: [ 76 | SettingsTile.navigation( 77 | leading: const Icon(Icons.key), 78 | title: const Text('API Key'), 79 | value: Text(LocalStorageService().apiKey == '' 80 | ? 'Add your secret API key' 81 | : obscureApiKey(LocalStorageService().apiKey) 82 | ), 83 | onPressed: (context) async { 84 | _textFieldController.text = LocalStorageService().apiKey; 85 | var result = await openStringDialog(context, 'API Key', 'Open AI API Key like sk-........') ?? ''; 86 | LocalStorageService().apiKey = result; 87 | setState(() { 88 | apiKey = result; 89 | }); 90 | }, 91 | ), 92 | SettingsTile.navigation( 93 | leading: const Icon(Icons.group), 94 | title: const Text('Organization (optional)'), 95 | value: Text(LocalStorageService().organization == '' 96 | ? 'None' 97 | : LocalStorageService().organization 98 | ), 99 | onPressed: (context) async { 100 | _textFieldController.text = LocalStorageService().organization; 101 | var result = await openStringDialog(context, 'Organization (optional)', 'Organization ID like org-.......') ?? ''; 102 | LocalStorageService().organization = result; 103 | setState(() { 104 | organization = result; 105 | }); 106 | }, 107 | ), 108 | SettingsTile.navigation( 109 | leading: const Icon(Icons.flight_takeoff), 110 | title: const Text('API Host (optional)'), 111 | value: Text('Access ${stripTrailingSlash(LocalStorageService().apiHost) + '/v1/chat/completions'}', style: const TextStyle(overflow: TextOverflow.ellipsis)), 112 | onPressed: (context) async { 113 | _textFieldController.text = LocalStorageService().apiHost; 114 | var result = await openStringDialog(context, 'API Host (optional)', 'URL like https://api.openai.com') ?? ''; 115 | LocalStorageService().apiHost = result; 116 | setState(() { 117 | apiHost = result; 118 | }); 119 | }, 120 | ), 121 | SettingsTile.navigation( 122 | leading: const Icon(Icons.open_in_new), 123 | title: const Text('Manage API keys'), 124 | value: const Text('https://platform.openai.com/account/api-keys'), 125 | onPressed: (context) async { 126 | await launchUrl(Uri.parse('https://platform.openai.com/account/api-keys'), mode: LaunchMode.externalApplication); 127 | }, 128 | ), 129 | ], 130 | ), 131 | SettingsSection( 132 | title: const Text('Chat Parameters'), 133 | tiles: [ 134 | SettingsTile( 135 | leading: const Icon(Icons.view_in_ar), 136 | title: const Text('Model'), 137 | value: Text(LocalStorageService().model), 138 | trailing: PopupMenuButton( 139 | icon: const Icon(Icons.more_vert), 140 | itemBuilder: (context) { 141 | return const [ 142 | PopupMenuItem( 143 | value: 'gpt-3.5-turbo', 144 | child: Text('gpt-3.5-turbo'), 145 | ), 146 | PopupMenuItem( 147 | value: 'gpt-3.5-turbo-16k', 148 | child: Text('gpt-3.5-turbo-16k'), 149 | ), 150 | PopupMenuItem( 151 | value: 'gpt-4', 152 | child: Text('gpt-4'), 153 | ), 154 | PopupMenuItem( 155 | value: 'gpt-4-32k', 156 | child: Text('gpt-4-32k'), 157 | ) 158 | ]; 159 | }, 160 | onSelected: (value) async { 161 | LocalStorageService().model = value; 162 | setState(() { 163 | model = value; 164 | }); 165 | }, 166 | ), 167 | ), 168 | SettingsTile( 169 | leading: const Icon(Icons.history), 170 | title: const Text('History Limit'), 171 | value: Text(LocalStorageService().historyCount.toString()), 172 | trailing: PopupMenuButton( 173 | icon: const Icon(Icons.more_vert), 174 | itemBuilder: (context) { 175 | return const [ 176 | PopupMenuItem( 177 | value: '0', 178 | child: Text('0'), 179 | ), 180 | PopupMenuItem( 181 | value: '2', 182 | child: Text('2'), 183 | ), 184 | PopupMenuItem( 185 | value: '4', 186 | child: Text('4'), 187 | ), 188 | PopupMenuItem( 189 | value: '6', 190 | child: Text('6'), 191 | ), 192 | PopupMenuItem( 193 | value: '8', 194 | child: Text('8'), 195 | ), 196 | PopupMenuItem( 197 | value: '10', 198 | child: Text('10'), 199 | ) 200 | ]; 201 | }, 202 | onSelected: (value) async { 203 | int intValue = int.parse(value); 204 | LocalStorageService().historyCount = intValue; 205 | setState(() { 206 | historyCount = intValue; 207 | }); 208 | }, 209 | ), 210 | ), 211 | ] 212 | ), 213 | SettingsSection( 214 | title: const Text('Appearance'), 215 | tiles: [ 216 | SettingsTile( 217 | leading: const Icon(Icons.text_format), 218 | title: const Text('Render Mode'), 219 | value: Text(getRenderModeDescription(LocalStorageService().renderMode)), 220 | trailing: PopupMenuButton( 221 | icon: const Icon(Icons.more_vert), 222 | itemBuilder: (context) { 223 | return const [ 224 | PopupMenuItem( 225 | value: 'markdown', 226 | child: Text('Markdown'), 227 | ), 228 | PopupMenuItem( 229 | value: 'text', 230 | child: Text('Plain Text'), 231 | ) 232 | ]; 233 | }, 234 | onSelected: (value) async { 235 | LocalStorageService().renderMode = value; 236 | setState(() { 237 | renderMode = value; 238 | }); 239 | }, 240 | ), 241 | ), 242 | ] 243 | ), 244 | SettingsSection( 245 | title: const Text('About'), 246 | tiles: [ 247 | SettingsTile.navigation( 248 | leading: const Icon(Icons.home), 249 | title: const Text('GitHub Project'), 250 | value: const Text('https://github.com/hahastudio/FlutterChat'), 251 | onPressed: (context) async { 252 | await launchUrl(Uri.parse('https://github.com/hahastudio/FlutterChat'), mode: LaunchMode.externalApplication); 253 | }, 254 | ), 255 | ] 256 | ) 257 | ], 258 | ), 259 | ); 260 | } 261 | } -------------------------------------------------------------------------------- /lib/screens/tablet_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class TabletScreenPage extends StatelessWidget { 4 | final Widget sidebar; 5 | final Widget body; 6 | final TabletMainView mainView; 7 | 8 | const TabletScreenPage({ 9 | super.key, 10 | required this.sidebar, 11 | required this.body, 12 | this.mainView = TabletMainView.body 13 | }); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return LayoutBuilder( 18 | builder: (BuildContext context, BoxConstraints constraints) { 19 | if ((constraints.maxWidth / constraints.maxHeight > 0.75) && (constraints.maxHeight >= 800)) { 20 | return Row( 21 | children: [ 22 | SizedBox( 23 | width: 300, 24 | child: sidebar, 25 | ), 26 | const VerticalDivider(thickness: 1, width: 1), 27 | Expanded( 28 | child: body 29 | ), 30 | ], 31 | ); 32 | } else { 33 | return mainView == TabletMainView.body ? body : sidebar; 34 | } 35 | }, 36 | ); 37 | } 38 | } 39 | 40 | enum TabletMainView { 41 | sidebar, 42 | body 43 | } -------------------------------------------------------------------------------- /lib/services/chat_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import '../api/openai_api.dart'; 5 | import 'local_storage_service.dart'; 6 | import 'token_service.dart'; 7 | import '../models/models.dart'; 8 | 9 | class ChatService { 10 | 11 | const ChatService({ 12 | required OpenAiApi apiServer, 13 | }) : _apiServer = apiServer; 14 | 15 | final OpenAiApi _apiServer; 16 | 17 | Conversation? getConversationById(String id) { 18 | var conversationJson = LocalStorageService().getConversationJsonById(id); 19 | if (conversationJson == '') 20 | return null; 21 | return Conversation.fromJson(jsonDecode(conversationJson)); 22 | } 23 | 24 | Future updateConversation(Conversation conversation) async { 25 | await LocalStorageService().setConversationJsonById(conversation.id, jsonEncode(conversation.toJson())); 26 | await _upsertConversationList(conversation); 27 | } 28 | 29 | Future removeConversationById(String id) async { 30 | await LocalStorageService().removeConversationJsonById(id); 31 | await _removeConversationFromListById(id); 32 | } 33 | 34 | List getConversationList() { 35 | return ConversationIndex.fromListJson(jsonDecode(LocalStorageService().conversationListJson)); 36 | } 37 | 38 | Future _upsertConversationList(Conversation conversation) async { 39 | var conversationList = getConversationList(); 40 | ConversationIndex c = ConversationIndex.fromConversation(conversation); 41 | conversationList.removeWhere((e) => e.id == conversation.id); 42 | conversationList.insert(0, c); 43 | conversationList.sort((a,b) => b.lastUpdated.compareTo(a.lastUpdated)); 44 | LocalStorageService().conversationListJson = jsonEncode(conversationList.map((i) => i.toJson()).toList()); 45 | } 46 | 47 | Future _removeConversationFromListById(String id) async { 48 | var conversationList = getConversationList(); 49 | conversationList.removeWhere((e) => e.id == id); 50 | LocalStorageService().conversationListJson = jsonEncode(conversationList.map((i) => i.toJson()).toList()); 51 | } 52 | 53 | Future getResponseFromServer(Conversation conversation) async { 54 | conversation.error = ''; 55 | 56 | var systemMessage = ChatMessage('system', conversation.systemMessage); 57 | var messages = TokenService.getEffectiveMessages(conversation, '').map((e) => e.toChatMessage()).toList(); 58 | messages.insert(0, systemMessage); 59 | 60 | try { 61 | var response = await _apiServer.chatCompletion(messages); 62 | conversation.messages.add(ConversationMessage.fromChatMessage(response.choices[0].message)); 63 | } catch (e) { 64 | // drop 'Exception: ' 65 | conversation.error = e.toString(); 66 | if (conversation.error.startsWith('Exception: ')) 67 | conversation.error = conversation.error.substring(11); 68 | conversation.messages.last.isError = true; 69 | } 70 | 71 | conversation.lastUpdated = DateTime.now(); 72 | updateConversation(conversation); 73 | 74 | return conversation; 75 | } 76 | 77 | Future _handleErrorGetResponseStream(dynamic error, Conversation conversation) async { 78 | conversation.error = error.toString(); 79 | if (conversation.error.startsWith('Exception: ')) 80 | conversation.error = conversation.error.substring(11); 81 | conversation.messages.last.isError = true; 82 | conversation.lastUpdated = DateTime.now(); 83 | await updateConversation(conversation); 84 | } 85 | 86 | Stream getResponseStreamFromServer(Conversation conversation) { 87 | final conversationStream = StreamController(); 88 | 89 | conversation.error = ''; 90 | 91 | var systemMessage = ChatMessage('system', conversation.systemMessage); 92 | var messages = TokenService.getEffectiveMessages(conversation, '').map((e) => e.toChatMessage()).toList(); 93 | messages.insert(0, systemMessage); 94 | 95 | var responseStream = _apiServer.chatCompletionStream(messages); 96 | responseStream.listen((chatStream) { 97 | if (chatStream.choices[0].delta.role.isNotEmpty) 98 | conversation.messages.add(ConversationMessage(chatStream.choices[0].delta.role, '')); 99 | if (chatStream.choices[0].delta.content.isNotEmpty) 100 | conversation.messages.last.content += chatStream.choices[0].delta.content; 101 | conversation.lastUpdated = DateTime.now(); 102 | conversationStream.add(conversation); 103 | }, 104 | onDone: () async { 105 | await updateConversation(conversation); 106 | conversationStream.close(); 107 | }, 108 | onError: (error) async { 109 | await _handleErrorGetResponseStream(error, conversation); 110 | conversationStream.add(conversation); 111 | }); 112 | 113 | return conversationStream.stream; 114 | } 115 | } -------------------------------------------------------------------------------- /lib/services/local_storage_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:shared_preferences/shared_preferences.dart'; 2 | 3 | import '../models/models.dart'; 4 | 5 | class LocalStorageService { 6 | static final LocalStorageService _instance = LocalStorageService._internal(); 7 | factory LocalStorageService() => _instance; 8 | 9 | LocalStorageService._internal(); 10 | 11 | late SharedPreferences _prefs; 12 | 13 | Future init() async { 14 | _prefs = await SharedPreferences.getInstance(); 15 | } 16 | 17 | static const prefApiKey = 'pref_apikey'; 18 | static const prefOrganization = 'pref_organization'; 19 | static const prefApiHost = 'pref_apiHost'; 20 | static const prefModel = 'pref_model'; 21 | static const prefHistoryCount = 'pref_historyCount'; 22 | static const prefRenderMode = 'pref_renderMode'; 23 | 24 | static const storeConversationList = 'store_conversations'; 25 | static const storeConversationPrefix = 'store_conversation_'; 26 | 27 | // preferences 28 | 29 | String get apiKey => _prefs.getString(prefApiKey) ?? ''; 30 | 31 | set apiKey(String value) { 32 | (() async { 33 | await _prefs.setString(prefApiKey, value); 34 | })(); 35 | } 36 | 37 | String get organization => _prefs.getString(prefOrganization) ?? ''; 38 | 39 | set organization(String value) { 40 | (() async { 41 | await _prefs.setString(prefOrganization, value); 42 | })(); 43 | } 44 | 45 | String get apiHost { 46 | var result = _prefs.getString(prefApiHost); 47 | if ((result == null) || (result.isEmpty)) 48 | return 'https://api.openai.com'; 49 | return result; 50 | } 51 | 52 | set apiHost(String value) { 53 | (() async { 54 | await _prefs.setString(prefApiHost, value); 55 | })(); 56 | } 57 | 58 | String get model => _prefs.getString(prefModel) ?? ChatRequest.defaultModel; 59 | 60 | set model(String value) { 61 | (() async { 62 | await _prefs.setString(prefModel, value); 63 | })(); 64 | } 65 | 66 | int get historyCount => _prefs.getInt(prefHistoryCount) ?? 4; 67 | 68 | set historyCount(int value) { 69 | (() async { 70 | await _prefs.setInt(prefHistoryCount, value); 71 | })(); 72 | } 73 | 74 | String get renderMode => _prefs.getString(prefRenderMode) ?? 'markdown'; 75 | 76 | set renderMode(String value) { 77 | (() async { 78 | await _prefs.setString(prefRenderMode, value); 79 | })(); 80 | } 81 | 82 | // storage 83 | 84 | String get conversationListJson => _prefs.getString(storeConversationList) ?? '[]'; 85 | 86 | set conversationListJson(String value) { 87 | (() async { 88 | await _prefs.setString(storeConversationList, value); 89 | })(); 90 | } 91 | 92 | String getConversationJsonById(String id) => _prefs.getString(storeConversationPrefix + id) ?? ''; 93 | 94 | Future setConversationJsonById(String id, String value) async { 95 | await _prefs.setString(storeConversationPrefix + id, value); 96 | } 97 | 98 | Future removeConversationJsonById(String id) async { 99 | await _prefs.remove(storeConversationPrefix + id); 100 | } 101 | } -------------------------------------------------------------------------------- /lib/services/token_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:tiktoken/tiktoken.dart'; 2 | 3 | import '../models/models.dart'; 4 | import 'local_storage_service.dart'; 5 | 6 | class TokenService { 7 | static const Map _tokenLimit = { 8 | 'gpt-3.5-turbo': 4096, 9 | 'gpt-3.5-turbo-16k': 16384, 10 | 'gpt-4': 8192, 11 | 'gpt-4-32k': 32768 12 | }; 13 | 14 | static int getTokenLimit() { 15 | return _tokenLimit[LocalStorageService().model] ?? 0; 16 | } 17 | 18 | static int getToken(String message) { 19 | final encoding = encodingForModel(LocalStorageService().model); 20 | if (message.isNotEmpty) { 21 | return encoding.encode(message).length; 22 | } 23 | return 0; 24 | } 25 | 26 | static int getMessagesToken(Conversation conversation) { 27 | int tokens = 0; 28 | for (final m in conversation.messages) { 29 | if (m.content.isNotEmpty) { 30 | tokens += getToken(m.content); 31 | } 32 | } 33 | return tokens; 34 | } 35 | 36 | static List getEffectiveMessages(Conversation conversation, String pendingMessage) { 37 | if (conversation.messages.isEmpty) 38 | return []; 39 | int remainingToken = getTokenLimit() - getToken(conversation.systemMessage) - getToken(pendingMessage); 40 | if (remainingToken <= 0) 41 | return []; 42 | int historyCount = LocalStorageService().historyCount; 43 | // newest user message doesn't belong to history 44 | if ((pendingMessage.isNotEmpty) || (conversation.messages.isNotEmpty && conversation.messages.last.role == 'user')) 45 | historyCount += 1; 46 | List effectiveMessages = []; 47 | for (final m in conversation.messages.reversed) { 48 | if (effectiveMessages.length >= historyCount) 49 | break; 50 | if (m.content.isEmpty) { 51 | effectiveMessages.insert(0, m); 52 | } else { 53 | var token = getToken(m.content); 54 | if (remainingToken < token) { 55 | break; 56 | } else { 57 | effectiveMessages.insert(0, m); 58 | remainingToken -= token; 59 | } 60 | } 61 | } 62 | return effectiveMessages; 63 | } 64 | 65 | static int getEffectiveMessagesToken(Conversation conversation, String pendingMessage) { 66 | if (conversation.messages.isEmpty) 67 | return 0; 68 | int remainingToken = getTokenLimit() - getToken(conversation.systemMessage) - getToken(pendingMessage); 69 | if (remainingToken <= 0) 70 | return 0; 71 | int effectiveToken = 0; 72 | int historyCount = LocalStorageService().historyCount; 73 | // newest user message doesn't belong to history 74 | if ((pendingMessage.isNotEmpty) || (conversation.messages.isNotEmpty && conversation.messages.last.role == 'user')) 75 | historyCount += 1; 76 | List effectiveMessages = []; 77 | for (final m in conversation.messages.reversed) { 78 | if (effectiveMessages.length >= historyCount) 79 | break; 80 | if (m.content.isEmpty) { 81 | effectiveMessages.insert(0, m); 82 | } else { 83 | var token = getToken(m.content); 84 | if (remainingToken < token) { 85 | break; 86 | } else { 87 | effectiveMessages.insert(0, m); 88 | remainingToken -= token; 89 | effectiveToken += token; 90 | } 91 | } 92 | } 93 | return effectiveToken; 94 | } 95 | } -------------------------------------------------------------------------------- /lib/util/extend_http_client.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:http/http.dart' as http; 4 | import 'package:http/retry.dart'; 5 | 6 | class SafeHttpClient extends http.BaseClient { 7 | final RetryClient _inner; 8 | 9 | SafeHttpClient(http.Client httpClient) : 10 | _inner = RetryClient(httpClient, 11 | when: (response) => response.statusCode >= 500 12 | ); 13 | 14 | @override 15 | Future send(http.BaseRequest request) { 16 | if (request.headers.containsKey('user-agent')) { 17 | request.headers.remove('user-agent'); 18 | } 19 | return _inner.send(request); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/util/string_util.dart: -------------------------------------------------------------------------------- 1 | String stripTrailingSlash(String path) { 2 | if (path.endsWith('/')) { 3 | return path.substring(0, path.length - 1); 4 | } 5 | return path; 6 | } -------------------------------------------------------------------------------- /lib/util/type_converter.dart: -------------------------------------------------------------------------------- 1 | /// converts values of type int to double 2 | /// intended to use while parsing json values where type will be dynamic 3 | /// returns value of type double 4 | intToDouble(dynamic val) { 5 | if (val.runtimeType == double) { 6 | return val; 7 | } else if (val.runtimeType == int) { 8 | return val.toDouble(); 9 | } else if (val == null) { 10 | return null; 11 | } else { 12 | throw Exception("value is not of type 'int' or 'double' got type '${val.runtimeType}'"); 13 | } 14 | } 15 | 16 | /// converts values of type double to int 17 | /// intended to use while parsing json values where type will be dynamic 18 | /// returns value of type int 19 | doubleToInt(dynamic val) { 20 | if (val.runtimeType == int) { 21 | return val; 22 | } else if (val.runtimeType == double) { 23 | return val.round(); 24 | } else if (val == null) { 25 | return null; 26 | } else { 27 | throw Exception("value is not of type 'int' or 'double' got type '${val.runtimeType}'"); 28 | } 29 | } -------------------------------------------------------------------------------- /lib/widgets/chat_message_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:flutter_markdown/flutter_markdown.dart'; 4 | 5 | import '../models/models.dart'; 6 | 7 | class ChatMessageWidget extends StatefulWidget { 8 | final ConversationMessage message; 9 | final bool isMarkdown; 10 | 11 | const ChatMessageWidget({super.key, required this.message, this.isMarkdown = true}); 12 | 13 | @override 14 | State createState() => _ChatMessageWidgetState(); 15 | } 16 | 17 | class _ChatMessageWidgetState extends State { 18 | bool _showContextMenu = false; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return Container( 23 | padding: const EdgeInsets.all(10), 24 | color: widget.message.role == 'user' ? 25 | Color.lerp(Theme.of(context).colorScheme.background, Colors.white, 0.1) 26 | : Color.lerp(Theme.of(context).colorScheme.background, Colors.white, 0.2), 27 | child: Row( 28 | crossAxisAlignment: CrossAxisAlignment.start, 29 | children: [ 30 | GestureDetector( 31 | onTap: () { 32 | setState(() { 33 | _showContextMenu = !_showContextMenu; 34 | }); 35 | }, 36 | child: SizedBox( 37 | width: 32, 38 | child: Column( 39 | children: [ 40 | Icon( 41 | widget.message.role == 'user'? Icons.account_circle : Icons.smart_toy, 42 | size: 32 43 | ), 44 | if (_showContextMenu) const SizedBox(height: 16), 45 | if (_showContextMenu) IconButton( 46 | icon: const Icon(Icons.content_copy, size: 20), 47 | onPressed: () async { 48 | await Clipboard.setData(ClipboardData(text: widget.message.content)); 49 | } 50 | ) 51 | ], 52 | ), 53 | ), 54 | ), 55 | const SizedBox(width: 16), 56 | Expanded( 57 | child: widget.isMarkdown ? 58 | MarkdownBody(data: widget.message.content, selectable: true) 59 | : SelectableText(widget.message.content) 60 | ) 61 | ], 62 | ) 63 | ); 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /lib/widgets/confirm_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ConfirmDialog extends StatelessWidget { 4 | final String title; 5 | final String content; 6 | 7 | const ConfirmDialog({ 8 | required this.title, 9 | required this.content, 10 | super.key 11 | }); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return AlertDialog( 16 | title: Text(title), 17 | content: Text(content), 18 | actions: [ 19 | TextButton( 20 | child: const Text('Cancel'), 21 | onPressed: () => Navigator.pop(context, false), 22 | ), 23 | ElevatedButton( 24 | child: const Text('OK'), 25 | onPressed: () => Navigator.pop(context, true), 26 | ), 27 | ], 28 | ); 29 | } 30 | } -------------------------------------------------------------------------------- /lib/widgets/conversation_edit_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../models/models.dart'; 4 | 5 | class ConversationEditDialog extends StatefulWidget { 6 | final Conversation conversation; 7 | final bool isEdit; 8 | 9 | const ConversationEditDialog({ 10 | required this.conversation, 11 | this.isEdit = false, 12 | super.key 13 | }); 14 | 15 | @override 16 | State createState() => _ConversationEditDialogState(); 17 | } 18 | 19 | class _ConversationEditDialogState extends State { 20 | late GlobalKey _formKey; 21 | late TextEditingController _titleEditingController; 22 | late TextEditingController _systemMessageEditingController; 23 | 24 | @override 25 | void initState() { 26 | _formKey = GlobalKey(); 27 | _titleEditingController = TextEditingController(); 28 | _systemMessageEditingController = TextEditingController(); 29 | super.initState(); 30 | } 31 | 32 | @override 33 | void dispose() { 34 | _titleEditingController.dispose(); 35 | _systemMessageEditingController.dispose(); 36 | super.dispose(); 37 | } 38 | 39 | @override 40 | Widget build(BuildContext context) { 41 | _titleEditingController.text = widget.conversation.title; 42 | _systemMessageEditingController.text = widget.conversation.systemMessage; 43 | 44 | return AlertDialog( 45 | content: Form( 46 | key: _formKey, 47 | child: Column( 48 | mainAxisSize: MainAxisSize.min, 49 | children: [ 50 | TextFormField( 51 | controller: _titleEditingController, 52 | validator: (value) { 53 | return value != null && value.isEmpty ? 'Title should not be empty' : null; 54 | }, 55 | decoration: const InputDecoration(hintText: 'Enter a conversation title'), 56 | ), 57 | TextFormField( 58 | controller: _systemMessageEditingController, 59 | maxLines: 3, 60 | decoration: const InputDecoration(hintText: 'Message to help set the behavior of the assistant'), 61 | ), 62 | ], 63 | ) 64 | ), 65 | title: Text(widget.isEdit? 'Edit conversation' : 'New conversation'), 66 | actions: [ 67 | TextButton( 68 | child: const Text('Cancel'), 69 | onPressed: () => Navigator.of(context).pop(), 70 | ), 71 | ElevatedButton( 72 | child: const Text('OK'), 73 | onPressed: () { 74 | if (_formKey.currentState == null || !_formKey.currentState!.validate()) 75 | return; 76 | widget.conversation.title = _titleEditingController.text; 77 | widget.conversation.systemMessage = _systemMessageEditingController.text; 78 | if (!widget.isEdit) 79 | widget.conversation.lastUpdated = DateTime.now(); 80 | Navigator.of(context).pop(widget.conversation); 81 | }, 82 | ), 83 | ], 84 | ); 85 | } 86 | 87 | } -------------------------------------------------------------------------------- /lib/widgets/conversation_list_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | import '../bloc/blocs.dart'; 5 | import '../models/models.dart'; 6 | import '../screens/screens.dart'; 7 | import '../services/chat_service.dart'; 8 | import '../widgets/widgets.dart'; 9 | 10 | class ConversationListWidget extends StatefulWidget { 11 | final Conversation? selectedConversation; 12 | 13 | const ConversationListWidget({super.key, this.selectedConversation}); 14 | 15 | @override 16 | State createState() => _ConversationListWidgetState(); 17 | } 18 | 19 | class _ConversationListWidgetState extends State { 20 | Conversation? selectedConversation; 21 | late ScrollController _scrollController; 22 | 23 | @override 24 | void initState() { 25 | _scrollController = ScrollController(); 26 | selectedConversation = widget.selectedConversation; 27 | super.initState(); 28 | } 29 | 30 | @override 31 | void dispose() { 32 | _scrollController.dispose(); 33 | super.dispose(); 34 | } 35 | 36 | Future showConversationDialog(BuildContext context, bool isEdit, Conversation conversation) => showDialog( 37 | context: context, 38 | builder: (context) { 39 | return ConversationEditDialog(conversation: conversation, isEdit: isEdit); 40 | } 41 | ); 42 | 43 | Future showDeleteConfirmDialog(BuildContext context) => showDialog( 44 | context: context, 45 | builder: (BuildContext context) { 46 | return const ConfirmDialog( 47 | title: 'Delete conversation', 48 | content: 'Would you like to delete the conversation?', 49 | ); 50 | }, 51 | ); 52 | 53 | @override 54 | Widget build(BuildContext context) { 55 | var chatService = context.read(); 56 | var bloc = BlocProvider.of(context); 57 | final state = context.watch().state; 58 | var conversations = state.conversations; 59 | 60 | return Flexible( 61 | child: ListView.builder( 62 | controller: _scrollController, 63 | itemCount: conversations.length, 64 | itemBuilder: (context, index) { 65 | var conversationIndex = conversations[index]; 66 | return ListTile( 67 | title: Text(conversationIndex.title, style: const TextStyle(overflow: TextOverflow.ellipsis)), 68 | selected: conversations[index].id == selectedConversation?.id, 69 | selectedTileColor: Color.lerp(Theme.of(context).colorScheme.background, Colors.white, 0.2), 70 | onTap: () async { 71 | var id = conversations[index].id; 72 | var conversation = chatService.getConversationById(id); 73 | if (conversation != null) { 74 | if (context.mounted) { 75 | if (Navigator.of(context).canPop()) { 76 | Navigator.of(context).pushReplacement(ChatScreenPage.route(conversation)); 77 | } else { 78 | Navigator.of(context).push(ChatScreenPage.route(conversation)); 79 | } 80 | } 81 | } 82 | }, 83 | trailing: conversations[index].id == selectedConversation?.id ? null : PopupMenuButton( 84 | icon: const Icon(Icons.more_vert), 85 | itemBuilder: (context) { 86 | return const [ 87 | PopupMenuItem( 88 | value: 'edit', 89 | child: Text('Edit'), 90 | ), 91 | PopupMenuItem( 92 | value: 'delete', 93 | child: Text('Delete'), 94 | ), 95 | ]; 96 | }, 97 | onSelected: (value) async { 98 | switch (value) { 99 | case 'edit': 100 | var id = conversations[index].id; 101 | var conversation = chatService.getConversationById(id); 102 | if (conversation == null) 103 | break; 104 | var newConversation = await showConversationDialog(context, true, conversation); 105 | if (newConversation != null) { 106 | await chatService.updateConversation(newConversation); 107 | bloc.add(const ConversationsRequested()); 108 | } 109 | break; 110 | case 'delete': 111 | var result = await showDeleteConfirmDialog(context); 112 | if (result == true) { 113 | if (context.mounted && (conversations[index].id == selectedConversation?.id)) 114 | Navigator.popUntil(context, (Route route) => route.isFirst); 115 | bloc.add(ConversationDeleted(conversations[index])); 116 | } 117 | break; 118 | default: 119 | break; 120 | } 121 | }, 122 | ), 123 | ); 124 | }, 125 | ) 126 | ); 127 | } 128 | 129 | } -------------------------------------------------------------------------------- /lib/widgets/empty_chat_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class EmptyChatWidget extends StatelessWidget { 4 | const EmptyChatWidget({super.key}); 5 | 6 | @override 7 | Widget build(BuildContext context) { 8 | return Scaffold( 9 | body: SafeArea( 10 | child: Center( 11 | child: Column( 12 | mainAxisAlignment: MainAxisAlignment.center, 13 | children: const [ 14 | Icon( 15 | Icons.chat, 16 | size: 128, 17 | ), 18 | Text('Create or choose a conversation to start') 19 | ], 20 | ), 21 | ), 22 | ) 23 | ); 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /lib/widgets/widgets.dart: -------------------------------------------------------------------------------- 1 | export 'chat_message_widget.dart'; 2 | export 'confirm_dialog.dart'; 3 | export 'conversation_edit_dialog.dart'; 4 | export 'conversation_list_widget.dart'; 5 | export 'empty_chat_widget.dart'; -------------------------------------------------------------------------------- /macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import shared_preferences_foundation 9 | import url_launcher_macos 10 | 11 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 12 | SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) 13 | UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 14 | } 15 | -------------------------------------------------------------------------------- /macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.14' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_macos_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | use_modular_headers! 32 | 33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) 34 | end 35 | 36 | post_install do |installer| 37 | installer.pods_project.targets.each do |target| 38 | flutter_additional_macos_build_settings(target) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /macos/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - FlutterMacOS (1.0.0) 3 | - shared_preferences_foundation (0.0.1): 4 | - Flutter 5 | - FlutterMacOS 6 | - url_launcher_macos (0.0.1): 7 | - FlutterMacOS 8 | 9 | DEPENDENCIES: 10 | - FlutterMacOS (from `Flutter/ephemeral`) 11 | - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/macos`) 12 | - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) 13 | 14 | EXTERNAL SOURCES: 15 | FlutterMacOS: 16 | :path: Flutter/ephemeral 17 | shared_preferences_foundation: 18 | :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/macos 19 | url_launcher_macos: 20 | :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos 21 | 22 | SPEC CHECKSUMS: 23 | FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 24 | shared_preferences_foundation: 986fc17f3d3251412d18b0265f9c64113a8c2472 25 | url_launcher_macos: 5335912b679c073563f29d89d33d10d459f95451 26 | 27 | PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7 28 | 29 | COCOAPODS: 1.11.3 30 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXAggregateTarget section */ 10 | 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { 11 | isa = PBXAggregateTarget; 12 | buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; 13 | buildPhases = ( 14 | 33CC111E2044C6BF0003C045 /* ShellScript */, 15 | ); 16 | dependencies = ( 17 | ); 18 | name = "Flutter Assemble"; 19 | productName = FLX; 20 | }; 21 | /* End PBXAggregateTarget section */ 22 | 23 | /* Begin PBXBuildFile section */ 24 | 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 25 | 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; 26 | 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 27 | 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 28 | 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; 29 | 3E8C053C7652BD6C9201130E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1C1E669B064361EED90139AC /* Pods_Runner.framework */; }; 30 | /* End PBXBuildFile section */ 31 | 32 | /* Begin PBXContainerItemProxy section */ 33 | 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { 34 | isa = PBXContainerItemProxy; 35 | containerPortal = 33CC10E52044A3C60003C045 /* Project object */; 36 | proxyType = 1; 37 | remoteGlobalIDString = 33CC111A2044C6BA0003C045; 38 | remoteInfo = FLX; 39 | }; 40 | /* End PBXContainerItemProxy section */ 41 | 42 | /* Begin PBXCopyFilesBuildPhase section */ 43 | 33CC110E2044A8840003C045 /* Bundle Framework */ = { 44 | isa = PBXCopyFilesBuildPhase; 45 | buildActionMask = 2147483647; 46 | dstPath = ""; 47 | dstSubfolderSpec = 10; 48 | files = ( 49 | ); 50 | name = "Bundle Framework"; 51 | runOnlyForDeploymentPostprocessing = 0; 52 | }; 53 | /* End PBXCopyFilesBuildPhase section */ 54 | 55 | /* Begin PBXFileReference section */ 56 | 1C1E669B064361EED90139AC /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 57 | 279C49876D767CD41BA85E2B /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 58 | 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 59 | 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; 60 | 33CC10ED2044A3C60003C045 /* flutter_chat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = flutter_chat.app; sourceTree = BUILT_PRODUCTS_DIR; }; 61 | 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 62 | 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 63 | 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 64 | 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; 65 | 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; 66 | 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; 67 | 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; 68 | 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; 69 | 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 70 | 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 71 | 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; 72 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 73 | 8DFAAA3250308FFF54699482 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 74 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; 75 | E360D36130A5815278999979 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 76 | /* End PBXFileReference section */ 77 | 78 | /* Begin PBXFrameworksBuildPhase section */ 79 | 33CC10EA2044A3C60003C045 /* Frameworks */ = { 80 | isa = PBXFrameworksBuildPhase; 81 | buildActionMask = 2147483647; 82 | files = ( 83 | 3E8C053C7652BD6C9201130E /* Pods_Runner.framework in Frameworks */, 84 | ); 85 | runOnlyForDeploymentPostprocessing = 0; 86 | }; 87 | /* End PBXFrameworksBuildPhase section */ 88 | 89 | /* Begin PBXGroup section */ 90 | 22AB494A39ED87C02865A8AA /* Pods */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | 8DFAAA3250308FFF54699482 /* Pods-Runner.debug.xcconfig */, 94 | E360D36130A5815278999979 /* Pods-Runner.release.xcconfig */, 95 | 279C49876D767CD41BA85E2B /* Pods-Runner.profile.xcconfig */, 96 | ); 97 | name = Pods; 98 | path = Pods; 99 | sourceTree = ""; 100 | }; 101 | 33BA886A226E78AF003329D5 /* Configs */ = { 102 | isa = PBXGroup; 103 | children = ( 104 | 33E5194F232828860026EE4D /* AppInfo.xcconfig */, 105 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 106 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 107 | 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, 108 | ); 109 | path = Configs; 110 | sourceTree = ""; 111 | }; 112 | 33CC10E42044A3C60003C045 = { 113 | isa = PBXGroup; 114 | children = ( 115 | 33FAB671232836740065AC1E /* Runner */, 116 | 33CEB47122A05771004F2AC0 /* Flutter */, 117 | 33CC10EE2044A3C60003C045 /* Products */, 118 | D73912EC22F37F3D000D13A0 /* Frameworks */, 119 | 22AB494A39ED87C02865A8AA /* Pods */, 120 | ); 121 | sourceTree = ""; 122 | }; 123 | 33CC10EE2044A3C60003C045 /* Products */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | 33CC10ED2044A3C60003C045 /* flutter_chat.app */, 127 | ); 128 | name = Products; 129 | sourceTree = ""; 130 | }; 131 | 33CC11242044D66E0003C045 /* Resources */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | 33CC10F22044A3C60003C045 /* Assets.xcassets */, 135 | 33CC10F42044A3C60003C045 /* MainMenu.xib */, 136 | 33CC10F72044A3C60003C045 /* Info.plist */, 137 | ); 138 | name = Resources; 139 | path = ..; 140 | sourceTree = ""; 141 | }; 142 | 33CEB47122A05771004F2AC0 /* Flutter */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, 146 | 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 147 | 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, 148 | 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, 149 | ); 150 | path = Flutter; 151 | sourceTree = ""; 152 | }; 153 | 33FAB671232836740065AC1E /* Runner */ = { 154 | isa = PBXGroup; 155 | children = ( 156 | 33CC10F02044A3C60003C045 /* AppDelegate.swift */, 157 | 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, 158 | 33E51913231747F40026EE4D /* DebugProfile.entitlements */, 159 | 33E51914231749380026EE4D /* Release.entitlements */, 160 | 33CC11242044D66E0003C045 /* Resources */, 161 | 33BA886A226E78AF003329D5 /* Configs */, 162 | ); 163 | path = Runner; 164 | sourceTree = ""; 165 | }; 166 | D73912EC22F37F3D000D13A0 /* Frameworks */ = { 167 | isa = PBXGroup; 168 | children = ( 169 | 1C1E669B064361EED90139AC /* Pods_Runner.framework */, 170 | ); 171 | name = Frameworks; 172 | sourceTree = ""; 173 | }; 174 | /* End PBXGroup section */ 175 | 176 | /* Begin PBXNativeTarget section */ 177 | 33CC10EC2044A3C60003C045 /* Runner */ = { 178 | isa = PBXNativeTarget; 179 | buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; 180 | buildPhases = ( 181 | 7C3DF9E8550CB5BFF39281E3 /* [CP] Check Pods Manifest.lock */, 182 | 33CC10E92044A3C60003C045 /* Sources */, 183 | 33CC10EA2044A3C60003C045 /* Frameworks */, 184 | 33CC10EB2044A3C60003C045 /* Resources */, 185 | 33CC110E2044A8840003C045 /* Bundle Framework */, 186 | 3399D490228B24CF009A79C7 /* ShellScript */, 187 | B9BD9AC6E261682588959D50 /* [CP] Embed Pods Frameworks */, 188 | ); 189 | buildRules = ( 190 | ); 191 | dependencies = ( 192 | 33CC11202044C79F0003C045 /* PBXTargetDependency */, 193 | ); 194 | name = Runner; 195 | productName = Runner; 196 | productReference = 33CC10ED2044A3C60003C045 /* flutter_chat.app */; 197 | productType = "com.apple.product-type.application"; 198 | }; 199 | /* End PBXNativeTarget section */ 200 | 201 | /* Begin PBXProject section */ 202 | 33CC10E52044A3C60003C045 /* Project object */ = { 203 | isa = PBXProject; 204 | attributes = { 205 | LastSwiftUpdateCheck = 0920; 206 | LastUpgradeCheck = 1300; 207 | ORGANIZATIONNAME = ""; 208 | TargetAttributes = { 209 | 33CC10EC2044A3C60003C045 = { 210 | CreatedOnToolsVersion = 9.2; 211 | LastSwiftMigration = 1100; 212 | ProvisioningStyle = Automatic; 213 | SystemCapabilities = { 214 | com.apple.Sandbox = { 215 | enabled = 1; 216 | }; 217 | }; 218 | }; 219 | 33CC111A2044C6BA0003C045 = { 220 | CreatedOnToolsVersion = 9.2; 221 | ProvisioningStyle = Manual; 222 | }; 223 | }; 224 | }; 225 | buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; 226 | compatibilityVersion = "Xcode 9.3"; 227 | developmentRegion = en; 228 | hasScannedForEncodings = 0; 229 | knownRegions = ( 230 | en, 231 | Base, 232 | ); 233 | mainGroup = 33CC10E42044A3C60003C045; 234 | productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; 235 | projectDirPath = ""; 236 | projectRoot = ""; 237 | targets = ( 238 | 33CC10EC2044A3C60003C045 /* Runner */, 239 | 33CC111A2044C6BA0003C045 /* Flutter Assemble */, 240 | ); 241 | }; 242 | /* End PBXProject section */ 243 | 244 | /* Begin PBXResourcesBuildPhase section */ 245 | 33CC10EB2044A3C60003C045 /* Resources */ = { 246 | isa = PBXResourcesBuildPhase; 247 | buildActionMask = 2147483647; 248 | files = ( 249 | 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, 250 | 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, 251 | ); 252 | runOnlyForDeploymentPostprocessing = 0; 253 | }; 254 | /* End PBXResourcesBuildPhase section */ 255 | 256 | /* Begin PBXShellScriptBuildPhase section */ 257 | 3399D490228B24CF009A79C7 /* ShellScript */ = { 258 | isa = PBXShellScriptBuildPhase; 259 | alwaysOutOfDate = 1; 260 | buildActionMask = 2147483647; 261 | files = ( 262 | ); 263 | inputFileListPaths = ( 264 | ); 265 | inputPaths = ( 266 | ); 267 | outputFileListPaths = ( 268 | ); 269 | outputPaths = ( 270 | ); 271 | runOnlyForDeploymentPostprocessing = 0; 272 | shellPath = /bin/sh; 273 | shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; 274 | }; 275 | 33CC111E2044C6BF0003C045 /* ShellScript */ = { 276 | isa = PBXShellScriptBuildPhase; 277 | buildActionMask = 2147483647; 278 | files = ( 279 | ); 280 | inputFileListPaths = ( 281 | Flutter/ephemeral/FlutterInputs.xcfilelist, 282 | ); 283 | inputPaths = ( 284 | Flutter/ephemeral/tripwire, 285 | ); 286 | outputFileListPaths = ( 287 | Flutter/ephemeral/FlutterOutputs.xcfilelist, 288 | ); 289 | outputPaths = ( 290 | ); 291 | runOnlyForDeploymentPostprocessing = 0; 292 | shellPath = /bin/sh; 293 | shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; 294 | }; 295 | 7C3DF9E8550CB5BFF39281E3 /* [CP] Check Pods Manifest.lock */ = { 296 | isa = PBXShellScriptBuildPhase; 297 | buildActionMask = 2147483647; 298 | files = ( 299 | ); 300 | inputFileListPaths = ( 301 | ); 302 | inputPaths = ( 303 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 304 | "${PODS_ROOT}/Manifest.lock", 305 | ); 306 | name = "[CP] Check Pods Manifest.lock"; 307 | outputFileListPaths = ( 308 | ); 309 | outputPaths = ( 310 | "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", 311 | ); 312 | runOnlyForDeploymentPostprocessing = 0; 313 | shellPath = /bin/sh; 314 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 315 | showEnvVarsInLog = 0; 316 | }; 317 | B9BD9AC6E261682588959D50 /* [CP] Embed Pods Frameworks */ = { 318 | isa = PBXShellScriptBuildPhase; 319 | buildActionMask = 2147483647; 320 | files = ( 321 | ); 322 | inputFileListPaths = ( 323 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", 324 | ); 325 | name = "[CP] Embed Pods Frameworks"; 326 | outputFileListPaths = ( 327 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", 328 | ); 329 | runOnlyForDeploymentPostprocessing = 0; 330 | shellPath = /bin/sh; 331 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; 332 | showEnvVarsInLog = 0; 333 | }; 334 | /* End PBXShellScriptBuildPhase section */ 335 | 336 | /* Begin PBXSourcesBuildPhase section */ 337 | 33CC10E92044A3C60003C045 /* Sources */ = { 338 | isa = PBXSourcesBuildPhase; 339 | buildActionMask = 2147483647; 340 | files = ( 341 | 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, 342 | 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, 343 | 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, 344 | ); 345 | runOnlyForDeploymentPostprocessing = 0; 346 | }; 347 | /* End PBXSourcesBuildPhase section */ 348 | 349 | /* Begin PBXTargetDependency section */ 350 | 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { 351 | isa = PBXTargetDependency; 352 | target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; 353 | targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; 354 | }; 355 | /* End PBXTargetDependency section */ 356 | 357 | /* Begin PBXVariantGroup section */ 358 | 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { 359 | isa = PBXVariantGroup; 360 | children = ( 361 | 33CC10F52044A3C60003C045 /* Base */, 362 | ); 363 | name = MainMenu.xib; 364 | path = Runner; 365 | sourceTree = ""; 366 | }; 367 | /* End PBXVariantGroup section */ 368 | 369 | /* Begin XCBuildConfiguration section */ 370 | 338D0CE9231458BD00FA5F75 /* Profile */ = { 371 | isa = XCBuildConfiguration; 372 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 373 | buildSettings = { 374 | ALWAYS_SEARCH_USER_PATHS = NO; 375 | CLANG_ANALYZER_NONNULL = YES; 376 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 377 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 378 | CLANG_CXX_LIBRARY = "libc++"; 379 | CLANG_ENABLE_MODULES = YES; 380 | CLANG_ENABLE_OBJC_ARC = YES; 381 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 382 | CLANG_WARN_BOOL_CONVERSION = YES; 383 | CLANG_WARN_CONSTANT_CONVERSION = YES; 384 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 385 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 386 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 387 | CLANG_WARN_EMPTY_BODY = YES; 388 | CLANG_WARN_ENUM_CONVERSION = YES; 389 | CLANG_WARN_INFINITE_RECURSION = YES; 390 | CLANG_WARN_INT_CONVERSION = YES; 391 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 392 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 393 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 394 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 395 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 396 | CODE_SIGN_IDENTITY = "-"; 397 | COPY_PHASE_STRIP = NO; 398 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 399 | ENABLE_NS_ASSERTIONS = NO; 400 | ENABLE_STRICT_OBJC_MSGSEND = YES; 401 | GCC_C_LANGUAGE_STANDARD = gnu11; 402 | GCC_NO_COMMON_BLOCKS = YES; 403 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 404 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 405 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 406 | GCC_WARN_UNUSED_FUNCTION = YES; 407 | GCC_WARN_UNUSED_VARIABLE = YES; 408 | MACOSX_DEPLOYMENT_TARGET = 10.14; 409 | MTL_ENABLE_DEBUG_INFO = NO; 410 | SDKROOT = macosx; 411 | SWIFT_COMPILATION_MODE = wholemodule; 412 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 413 | }; 414 | name = Profile; 415 | }; 416 | 338D0CEA231458BD00FA5F75 /* Profile */ = { 417 | isa = XCBuildConfiguration; 418 | baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; 419 | buildSettings = { 420 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 421 | CLANG_ENABLE_MODULES = YES; 422 | CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; 423 | CODE_SIGN_STYLE = Automatic; 424 | COMBINE_HIDPI_IMAGES = YES; 425 | INFOPLIST_FILE = Runner/Info.plist; 426 | LD_RUNPATH_SEARCH_PATHS = ( 427 | "$(inherited)", 428 | "@executable_path/../Frameworks", 429 | ); 430 | PROVISIONING_PROFILE_SPECIFIER = ""; 431 | SWIFT_VERSION = 5.0; 432 | }; 433 | name = Profile; 434 | }; 435 | 338D0CEB231458BD00FA5F75 /* Profile */ = { 436 | isa = XCBuildConfiguration; 437 | buildSettings = { 438 | CODE_SIGN_STYLE = Manual; 439 | PRODUCT_NAME = "$(TARGET_NAME)"; 440 | }; 441 | name = Profile; 442 | }; 443 | 33CC10F92044A3C60003C045 /* Debug */ = { 444 | isa = XCBuildConfiguration; 445 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 446 | buildSettings = { 447 | ALWAYS_SEARCH_USER_PATHS = NO; 448 | CLANG_ANALYZER_NONNULL = YES; 449 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 450 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 451 | CLANG_CXX_LIBRARY = "libc++"; 452 | CLANG_ENABLE_MODULES = YES; 453 | CLANG_ENABLE_OBJC_ARC = YES; 454 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 455 | CLANG_WARN_BOOL_CONVERSION = YES; 456 | CLANG_WARN_CONSTANT_CONVERSION = YES; 457 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 458 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 459 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 460 | CLANG_WARN_EMPTY_BODY = YES; 461 | CLANG_WARN_ENUM_CONVERSION = YES; 462 | CLANG_WARN_INFINITE_RECURSION = YES; 463 | CLANG_WARN_INT_CONVERSION = YES; 464 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 465 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 466 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 467 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 468 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 469 | CODE_SIGN_IDENTITY = "-"; 470 | COPY_PHASE_STRIP = NO; 471 | DEBUG_INFORMATION_FORMAT = dwarf; 472 | ENABLE_STRICT_OBJC_MSGSEND = YES; 473 | ENABLE_TESTABILITY = YES; 474 | GCC_C_LANGUAGE_STANDARD = gnu11; 475 | GCC_DYNAMIC_NO_PIC = NO; 476 | GCC_NO_COMMON_BLOCKS = YES; 477 | GCC_OPTIMIZATION_LEVEL = 0; 478 | GCC_PREPROCESSOR_DEFINITIONS = ( 479 | "DEBUG=1", 480 | "$(inherited)", 481 | ); 482 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 483 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 484 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 485 | GCC_WARN_UNUSED_FUNCTION = YES; 486 | GCC_WARN_UNUSED_VARIABLE = YES; 487 | MACOSX_DEPLOYMENT_TARGET = 10.14; 488 | MTL_ENABLE_DEBUG_INFO = YES; 489 | ONLY_ACTIVE_ARCH = YES; 490 | SDKROOT = macosx; 491 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 492 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 493 | }; 494 | name = Debug; 495 | }; 496 | 33CC10FA2044A3C60003C045 /* Release */ = { 497 | isa = XCBuildConfiguration; 498 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 499 | buildSettings = { 500 | ALWAYS_SEARCH_USER_PATHS = NO; 501 | CLANG_ANALYZER_NONNULL = YES; 502 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 503 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 504 | CLANG_CXX_LIBRARY = "libc++"; 505 | CLANG_ENABLE_MODULES = YES; 506 | CLANG_ENABLE_OBJC_ARC = YES; 507 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 508 | CLANG_WARN_BOOL_CONVERSION = YES; 509 | CLANG_WARN_CONSTANT_CONVERSION = YES; 510 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 511 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 512 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 513 | CLANG_WARN_EMPTY_BODY = YES; 514 | CLANG_WARN_ENUM_CONVERSION = YES; 515 | CLANG_WARN_INFINITE_RECURSION = YES; 516 | CLANG_WARN_INT_CONVERSION = YES; 517 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 518 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 519 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 520 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 521 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 522 | CODE_SIGN_IDENTITY = "-"; 523 | COPY_PHASE_STRIP = NO; 524 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 525 | ENABLE_NS_ASSERTIONS = NO; 526 | ENABLE_STRICT_OBJC_MSGSEND = YES; 527 | GCC_C_LANGUAGE_STANDARD = gnu11; 528 | GCC_NO_COMMON_BLOCKS = YES; 529 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 530 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 531 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 532 | GCC_WARN_UNUSED_FUNCTION = YES; 533 | GCC_WARN_UNUSED_VARIABLE = YES; 534 | MACOSX_DEPLOYMENT_TARGET = 10.14; 535 | MTL_ENABLE_DEBUG_INFO = NO; 536 | SDKROOT = macosx; 537 | SWIFT_COMPILATION_MODE = wholemodule; 538 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 539 | }; 540 | name = Release; 541 | }; 542 | 33CC10FC2044A3C60003C045 /* Debug */ = { 543 | isa = XCBuildConfiguration; 544 | baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; 545 | buildSettings = { 546 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 547 | CLANG_ENABLE_MODULES = YES; 548 | CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; 549 | CODE_SIGN_STYLE = Automatic; 550 | COMBINE_HIDPI_IMAGES = YES; 551 | INFOPLIST_FILE = Runner/Info.plist; 552 | LD_RUNPATH_SEARCH_PATHS = ( 553 | "$(inherited)", 554 | "@executable_path/../Frameworks", 555 | ); 556 | PROVISIONING_PROFILE_SPECIFIER = ""; 557 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 558 | SWIFT_VERSION = 5.0; 559 | }; 560 | name = Debug; 561 | }; 562 | 33CC10FD2044A3C60003C045 /* Release */ = { 563 | isa = XCBuildConfiguration; 564 | baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; 565 | buildSettings = { 566 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 567 | CLANG_ENABLE_MODULES = YES; 568 | CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; 569 | CODE_SIGN_STYLE = Automatic; 570 | COMBINE_HIDPI_IMAGES = YES; 571 | INFOPLIST_FILE = Runner/Info.plist; 572 | LD_RUNPATH_SEARCH_PATHS = ( 573 | "$(inherited)", 574 | "@executable_path/../Frameworks", 575 | ); 576 | PROVISIONING_PROFILE_SPECIFIER = ""; 577 | SWIFT_VERSION = 5.0; 578 | }; 579 | name = Release; 580 | }; 581 | 33CC111C2044C6BA0003C045 /* Debug */ = { 582 | isa = XCBuildConfiguration; 583 | buildSettings = { 584 | CODE_SIGN_STYLE = Manual; 585 | PRODUCT_NAME = "$(TARGET_NAME)"; 586 | }; 587 | name = Debug; 588 | }; 589 | 33CC111D2044C6BA0003C045 /* Release */ = { 590 | isa = XCBuildConfiguration; 591 | buildSettings = { 592 | CODE_SIGN_STYLE = Automatic; 593 | PRODUCT_NAME = "$(TARGET_NAME)"; 594 | }; 595 | name = Release; 596 | }; 597 | /* End XCBuildConfiguration section */ 598 | 599 | /* Begin XCConfigurationList section */ 600 | 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { 601 | isa = XCConfigurationList; 602 | buildConfigurations = ( 603 | 33CC10F92044A3C60003C045 /* Debug */, 604 | 33CC10FA2044A3C60003C045 /* Release */, 605 | 338D0CE9231458BD00FA5F75 /* Profile */, 606 | ); 607 | defaultConfigurationIsVisible = 0; 608 | defaultConfigurationName = Release; 609 | }; 610 | 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { 611 | isa = XCConfigurationList; 612 | buildConfigurations = ( 613 | 33CC10FC2044A3C60003C045 /* Debug */, 614 | 33CC10FD2044A3C60003C045 /* Release */, 615 | 338D0CEA231458BD00FA5F75 /* Profile */, 616 | ); 617 | defaultConfigurationIsVisible = 0; 618 | defaultConfigurationName = Release; 619 | }; 620 | 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { 621 | isa = XCConfigurationList; 622 | buildConfigurations = ( 623 | 33CC111C2044C6BA0003C045 /* Debug */, 624 | 33CC111D2044C6BA0003C045 /* Release */, 625 | 338D0CEB231458BD00FA5F75 /* Profile */, 626 | ); 627 | defaultConfigurationIsVisible = 0; 628 | defaultConfigurationName = Release; 629 | }; 630 | /* End XCConfigurationList section */ 631 | }; 632 | rootObject = 33CC10E52044A3C60003C045 /* Project object */; 633 | } 634 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @NSApplicationMain 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahastudio/FlutterChat/8634fb79431513b972539c49b7941640b36aeef4/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /macos/Runner/Base.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 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 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | -------------------------------------------------------------------------------- /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_chat 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.hahastudio.flutterchat.flutterChat 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2023 com.hahastudio.flutterchat. All rights reserved. 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | com.apple.security.network.client 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController.init() 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 | -------------------------------------------------------------------------------- /macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | args: 5 | dependency: transitive 6 | description: 7 | name: args 8 | sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "2.4.0" 12 | async: 13 | dependency: transitive 14 | description: 15 | name: async 16 | sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.10.0" 20 | bloc: 21 | dependency: "direct main" 22 | description: 23 | name: bloc 24 | sha256: "658a5ae59edcf1e58aac98b000a71c762ad8f46f1394c34a52050cafb3e11a80" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "8.1.1" 28 | boolean_selector: 29 | dependency: transitive 30 | description: 31 | name: boolean_selector 32 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "2.1.1" 36 | characters: 37 | dependency: transitive 38 | description: 39 | name: characters 40 | sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.2.1" 44 | clock: 45 | dependency: transitive 46 | description: 47 | name: clock 48 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.1.1" 52 | collection: 53 | dependency: transitive 54 | description: 55 | name: collection 56 | sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "1.17.0" 60 | crypto: 61 | dependency: transitive 62 | description: 63 | name: crypto 64 | sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "3.0.2" 68 | cupertino_icons: 69 | dependency: "direct main" 70 | description: 71 | name: cupertino_icons 72 | sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "1.0.5" 76 | equatable: 77 | dependency: "direct main" 78 | description: 79 | name: equatable 80 | sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "2.0.5" 84 | fake_async: 85 | dependency: transitive 86 | description: 87 | name: fake_async 88 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 89 | url: "https://pub.dev" 90 | source: hosted 91 | version: "1.3.1" 92 | ffi: 93 | dependency: transitive 94 | description: 95 | name: ffi 96 | sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 97 | url: "https://pub.dev" 98 | source: hosted 99 | version: "2.0.1" 100 | file: 101 | dependency: transitive 102 | description: 103 | name: file 104 | sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" 105 | url: "https://pub.dev" 106 | source: hosted 107 | version: "6.1.4" 108 | flutter: 109 | dependency: "direct main" 110 | description: flutter 111 | source: sdk 112 | version: "0.0.0" 113 | flutter_bloc: 114 | dependency: "direct main" 115 | description: 116 | name: flutter_bloc 117 | sha256: "434951eea948dbe87f737b674281465f610b8259c16c097b8163ce138749a775" 118 | url: "https://pub.dev" 119 | source: hosted 120 | version: "8.1.2" 121 | flutter_lints: 122 | dependency: "direct dev" 123 | description: 124 | name: flutter_lints 125 | sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c 126 | url: "https://pub.dev" 127 | source: hosted 128 | version: "2.0.1" 129 | flutter_markdown: 130 | dependency: "direct main" 131 | description: 132 | name: flutter_markdown 133 | sha256: "7b25c10de1fea883f3c4f9b8389506b54053cd00807beab69fd65c8653a2711f" 134 | url: "https://pub.dev" 135 | source: hosted 136 | version: "0.6.14" 137 | flutter_test: 138 | dependency: "direct dev" 139 | description: flutter 140 | source: sdk 141 | version: "0.0.0" 142 | flutter_web_plugins: 143 | dependency: transitive 144 | description: flutter 145 | source: sdk 146 | version: "0.0.0" 147 | http: 148 | dependency: "direct main" 149 | description: 150 | name: http 151 | sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" 152 | url: "https://pub.dev" 153 | source: hosted 154 | version: "0.13.5" 155 | http_parser: 156 | dependency: transitive 157 | description: 158 | name: http_parser 159 | sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" 160 | url: "https://pub.dev" 161 | source: hosted 162 | version: "4.0.2" 163 | js: 164 | dependency: transitive 165 | description: 166 | name: js 167 | sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" 168 | url: "https://pub.dev" 169 | source: hosted 170 | version: "0.6.5" 171 | lints: 172 | dependency: transitive 173 | description: 174 | name: lints 175 | sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" 176 | url: "https://pub.dev" 177 | source: hosted 178 | version: "2.0.1" 179 | markdown: 180 | dependency: transitive 181 | description: 182 | name: markdown 183 | sha256: d95a9d12954aafc97f984ca29baaa7690ed4d9ec4140a23ad40580bcdb6c87f5 184 | url: "https://pub.dev" 185 | source: hosted 186 | version: "7.0.2" 187 | matcher: 188 | dependency: transitive 189 | description: 190 | name: matcher 191 | sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" 192 | url: "https://pub.dev" 193 | source: hosted 194 | version: "0.12.13" 195 | material_color_utilities: 196 | dependency: transitive 197 | description: 198 | name: material_color_utilities 199 | sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 200 | url: "https://pub.dev" 201 | source: hosted 202 | version: "0.2.0" 203 | meta: 204 | dependency: transitive 205 | description: 206 | name: meta 207 | sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" 208 | url: "https://pub.dev" 209 | source: hosted 210 | version: "1.8.0" 211 | nested: 212 | dependency: transitive 213 | description: 214 | name: nested 215 | sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" 216 | url: "https://pub.dev" 217 | source: hosted 218 | version: "1.0.0" 219 | path: 220 | dependency: transitive 221 | description: 222 | name: path 223 | sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b 224 | url: "https://pub.dev" 225 | source: hosted 226 | version: "1.8.2" 227 | path_provider_linux: 228 | dependency: transitive 229 | description: 230 | name: path_provider_linux 231 | sha256: "2ae08f2216225427e64ad224a24354221c2c7907e448e6e0e8b57b1eb9f10ad1" 232 | url: "https://pub.dev" 233 | source: hosted 234 | version: "2.1.10" 235 | path_provider_platform_interface: 236 | dependency: transitive 237 | description: 238 | name: path_provider_platform_interface 239 | sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" 240 | url: "https://pub.dev" 241 | source: hosted 242 | version: "2.0.6" 243 | path_provider_windows: 244 | dependency: transitive 245 | description: 246 | name: path_provider_windows 247 | sha256: f53720498d5a543f9607db4b0e997c4b5438884de25b0f73098cc2671a51b130 248 | url: "https://pub.dev" 249 | source: hosted 250 | version: "2.1.5" 251 | platform: 252 | dependency: transitive 253 | description: 254 | name: platform 255 | sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" 256 | url: "https://pub.dev" 257 | source: hosted 258 | version: "3.1.0" 259 | plugin_platform_interface: 260 | dependency: transitive 261 | description: 262 | name: plugin_platform_interface 263 | sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" 264 | url: "https://pub.dev" 265 | source: hosted 266 | version: "2.1.4" 267 | process: 268 | dependency: transitive 269 | description: 270 | name: process 271 | sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" 272 | url: "https://pub.dev" 273 | source: hosted 274 | version: "4.2.4" 275 | provider: 276 | dependency: transitive 277 | description: 278 | name: provider 279 | sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f 280 | url: "https://pub.dev" 281 | source: hosted 282 | version: "6.0.5" 283 | settings_ui: 284 | dependency: "direct main" 285 | description: 286 | name: settings_ui 287 | sha256: d9838037cb554b24b4218b2d07666fbada3478882edefae375ee892b6c820ef3 288 | url: "https://pub.dev" 289 | source: hosted 290 | version: "2.0.2" 291 | shared_preferences: 292 | dependency: "direct main" 293 | description: 294 | name: shared_preferences 295 | sha256: "858aaa72d8f61637d64e776aca82e1c67e6d9ee07979123c5d17115031c1b13b" 296 | url: "https://pub.dev" 297 | source: hosted 298 | version: "2.1.0" 299 | shared_preferences_android: 300 | dependency: transitive 301 | description: 302 | name: shared_preferences_android 303 | sha256: "8304d8a1f7d21a429f91dee552792249362b68a331ac5c3c1caf370f658873f6" 304 | url: "https://pub.dev" 305 | source: hosted 306 | version: "2.1.0" 307 | shared_preferences_foundation: 308 | dependency: transitive 309 | description: 310 | name: shared_preferences_foundation 311 | sha256: cf2a42fb20148502022861f71698db12d937c7459345a1bdaa88fc91a91b3603 312 | url: "https://pub.dev" 313 | source: hosted 314 | version: "2.2.0" 315 | shared_preferences_linux: 316 | dependency: transitive 317 | description: 318 | name: shared_preferences_linux 319 | sha256: "9d387433ca65717bbf1be88f4d5bb18f10508917a8fa2fb02e0fd0d7479a9afa" 320 | url: "https://pub.dev" 321 | source: hosted 322 | version: "2.2.0" 323 | shared_preferences_platform_interface: 324 | dependency: transitive 325 | description: 326 | name: shared_preferences_platform_interface 327 | sha256: fb5cf25c0235df2d0640ac1b1174f6466bd311f621574997ac59018a6664548d 328 | url: "https://pub.dev" 329 | source: hosted 330 | version: "2.2.0" 331 | shared_preferences_web: 332 | dependency: transitive 333 | description: 334 | name: shared_preferences_web 335 | sha256: "74083203a8eae241e0de4a0d597dbedab3b8fef5563f33cf3c12d7e93c655ca5" 336 | url: "https://pub.dev" 337 | source: hosted 338 | version: "2.1.0" 339 | shared_preferences_windows: 340 | dependency: transitive 341 | description: 342 | name: shared_preferences_windows 343 | sha256: "5e588e2efef56916a3b229c3bfe81e6a525665a454519ca51dbcc4236a274173" 344 | url: "https://pub.dev" 345 | source: hosted 346 | version: "2.2.0" 347 | sky_engine: 348 | dependency: transitive 349 | description: flutter 350 | source: sdk 351 | version: "0.0.99" 352 | source_span: 353 | dependency: transitive 354 | description: 355 | name: source_span 356 | sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 357 | url: "https://pub.dev" 358 | source: hosted 359 | version: "1.9.1" 360 | stack_trace: 361 | dependency: transitive 362 | description: 363 | name: stack_trace 364 | sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 365 | url: "https://pub.dev" 366 | source: hosted 367 | version: "1.11.0" 368 | stream_channel: 369 | dependency: transitive 370 | description: 371 | name: stream_channel 372 | sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" 373 | url: "https://pub.dev" 374 | source: hosted 375 | version: "2.1.1" 376 | string_scanner: 377 | dependency: transitive 378 | description: 379 | name: string_scanner 380 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 381 | url: "https://pub.dev" 382 | source: hosted 383 | version: "1.2.0" 384 | term_glyph: 385 | dependency: transitive 386 | description: 387 | name: term_glyph 388 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 389 | url: "https://pub.dev" 390 | source: hosted 391 | version: "1.2.1" 392 | test_api: 393 | dependency: transitive 394 | description: 395 | name: test_api 396 | sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 397 | url: "https://pub.dev" 398 | source: hosted 399 | version: "0.4.16" 400 | tiktoken: 401 | dependency: "direct main" 402 | description: 403 | name: tiktoken 404 | sha256: a100ca6e387e18be7b711478c8d18627e0753536846600a5961a83631ce5c586 405 | url: "https://pub.dev" 406 | source: hosted 407 | version: "1.0.3" 408 | typed_data: 409 | dependency: transitive 410 | description: 411 | name: typed_data 412 | sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" 413 | url: "https://pub.dev" 414 | source: hosted 415 | version: "1.3.1" 416 | url_launcher: 417 | dependency: "direct main" 418 | description: 419 | name: url_launcher 420 | sha256: "75f2846facd11168d007529d6cd8fcb2b750186bea046af9711f10b907e1587e" 421 | url: "https://pub.dev" 422 | source: hosted 423 | version: "6.1.10" 424 | url_launcher_android: 425 | dependency: transitive 426 | description: 427 | name: url_launcher_android 428 | sha256: dd729390aa936bf1bdf5cd1bc7468ff340263f80a2c4f569416507667de8e3c8 429 | url: "https://pub.dev" 430 | source: hosted 431 | version: "6.0.26" 432 | url_launcher_ios: 433 | dependency: transitive 434 | description: 435 | name: url_launcher_ios 436 | sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" 437 | url: "https://pub.dev" 438 | source: hosted 439 | version: "6.1.4" 440 | url_launcher_linux: 441 | dependency: transitive 442 | description: 443 | name: url_launcher_linux 444 | sha256: "206fb8334a700ef7754d6a9ed119e7349bc830448098f21a69bf1b4ed038cabc" 445 | url: "https://pub.dev" 446 | source: hosted 447 | version: "3.0.4" 448 | url_launcher_macos: 449 | dependency: transitive 450 | description: 451 | name: url_launcher_macos 452 | sha256: "0ef2b4f97942a16523e51256b799e9aa1843da6c60c55eefbfa9dbc2dcb8331a" 453 | url: "https://pub.dev" 454 | source: hosted 455 | version: "3.0.4" 456 | url_launcher_platform_interface: 457 | dependency: transitive 458 | description: 459 | name: url_launcher_platform_interface 460 | sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370" 461 | url: "https://pub.dev" 462 | source: hosted 463 | version: "2.1.2" 464 | url_launcher_web: 465 | dependency: transitive 466 | description: 467 | name: url_launcher_web 468 | sha256: "81fe91b6c4f84f222d186a9d23c73157dc4c8e1c71489c4d08be1ad3b228f1aa" 469 | url: "https://pub.dev" 470 | source: hosted 471 | version: "2.0.16" 472 | url_launcher_windows: 473 | dependency: transitive 474 | description: 475 | name: url_launcher_windows 476 | sha256: a83ba3607a507758669cfafb03f9de09bf6e6280c14d9b9cb18f013e406dcacd 477 | url: "https://pub.dev" 478 | source: hosted 479 | version: "3.0.5" 480 | uuid: 481 | dependency: "direct main" 482 | description: 483 | name: uuid 484 | sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" 485 | url: "https://pub.dev" 486 | source: hosted 487 | version: "3.0.7" 488 | vector_math: 489 | dependency: transitive 490 | description: 491 | name: vector_math 492 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 493 | url: "https://pub.dev" 494 | source: hosted 495 | version: "2.1.4" 496 | win32: 497 | dependency: transitive 498 | description: 499 | name: win32 500 | sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4 501 | url: "https://pub.dev" 502 | source: hosted 503 | version: "3.1.4" 504 | xdg_directories: 505 | dependency: transitive 506 | description: 507 | name: xdg_directories 508 | sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1 509 | url: "https://pub.dev" 510 | source: hosted 511 | version: "1.0.0" 512 | sdks: 513 | dart: ">=2.19.3 <3.0.0" 514 | flutter: ">=3.3.0" 515 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_chat 2 | description: A new Flutter project which communicate with OpenAI API 3 | # The following line prevents the package from being accidentally published to 4 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 5 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 6 | 7 | # The following defines the version and build number for your application. 8 | # A version number is three numbers separated by dots, like 1.2.43 9 | # followed by an optional build number separated by a +. 10 | # Both the version and the builder number may be overridden in flutter 11 | # build by specifying --build-name and --build-number, respectively. 12 | # In Android, build-name is used as versionName while build-number used as versionCode. 13 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 14 | # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. 15 | # Read more about iOS versioning at 16 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 17 | # In Windows, build-name is used as the major, minor, and patch parts 18 | # of the product and file versions while build-number is used as the build suffix. 19 | version: 1.0.9+1 20 | 21 | environment: 22 | sdk: '>=2.19.3 <3.0.0' 23 | 24 | # Dependencies specify other packages that your package needs in order to work. 25 | # To automatically upgrade your package dependencies to the latest versions 26 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 27 | # dependencies can be manually updated by changing the version numbers below to 28 | # the latest version available on pub.dev. To see which dependencies have newer 29 | # versions available, run `flutter pub outdated`. 30 | dependencies: 31 | flutter: 32 | sdk: flutter 33 | 34 | 35 | # The following adds the Cupertino Icons font to your application. 36 | # Use with the CupertinoIcons class for iOS style icons. 37 | cupertino_icons: ^1.0.5 38 | http: ^0.13.5 39 | shared_preferences: ^2.0.18 40 | uuid: ^3.0.7 41 | settings_ui: ^2.0.2 42 | equatable: ^2.0.5 43 | bloc: ^8.1.1 44 | flutter_bloc: ^8.1.2 45 | flutter_markdown: ^0.6.14 46 | tiktoken: ^1.0.2 47 | url_launcher: ^6.1.10 48 | 49 | dev_dependencies: 50 | flutter_test: 51 | sdk: flutter 52 | 53 | # The "flutter_lints" package below contains a set of recommended lints to 54 | # encourage good coding practices. The lint set provided by the package is 55 | # activated in the `analysis_options.yaml` file located at the root of your 56 | # package. See that file for information about deactivating specific lint 57 | # rules and activating additional ones. 58 | flutter_lints: ^2.0.0 59 | 60 | # For information on the generic Dart part of this file, see the 61 | # following page: https://dart.dev/tools/pub/pubspec 62 | 63 | # The following section is specific to Flutter packages. 64 | flutter: 65 | 66 | # The following line ensures that the Material Icons font is 67 | # included with your application, so that you can use the icons in 68 | # the material Icons class. 69 | uses-material-design: true 70 | 71 | # To add assets to your application, add an assets section, like this: 72 | # assets: 73 | # - images/a_dot_burr.jpeg 74 | # - images/a_dot_ham.jpeg 75 | 76 | # An image asset can refer to one or more resolution-specific "variants", see 77 | # https://flutter.dev/assets-and-images/#resolution-aware 78 | 79 | # For details regarding adding assets from package dependencies, see 80 | # https://flutter.dev/assets-and-images/#from-packages 81 | 82 | # To add custom fonts to your application, add a fonts section here, 83 | # in this "flutter" section. Each entry in this list should have a 84 | # "family" key with the font family name, and a "fonts" key with a 85 | # list giving the asset and other descriptors for the font. For 86 | # example: 87 | # fonts: 88 | # - family: Schyler 89 | # fonts: 90 | # - asset: fonts/Schyler-Regular.ttf 91 | # - asset: fonts/Schyler-Italic.ttf 92 | # style: italic 93 | # - family: Trajan Pro 94 | # fonts: 95 | # - asset: fonts/TrajanPro.ttf 96 | # - asset: fonts/TrajanPro_Bold.ttf 97 | # weight: 700 98 | # 99 | # For details regarding fonts from package dependencies, 100 | # see https://flutter.dev/custom-fonts/#from-packages 101 | -------------------------------------------------------------------------------- /test/openai_api_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:flutter_chat/api/openai_api.dart'; 5 | import 'package:flutter_chat/models/chat.dart'; 6 | import 'package:flutter_chat/services/local_storage_service.dart'; 7 | import 'package:flutter_chat/util/extend_http_client.dart'; 8 | import 'package:flutter_test/flutter_test.dart'; 9 | import 'package:http/http.dart'; 10 | import 'package:http/testing.dart'; 11 | import 'package:shared_preferences/shared_preferences.dart'; 12 | 13 | void main() async { 14 | SharedPreferences.setMockInitialValues({ 15 | "pref_apikey": "deadbeef" 16 | }); 17 | await LocalStorageService().init(); 18 | 19 | group('Chat Completion', () { 20 | test('return messages when http response is successful', () async { 21 | final mockHTTPClient = MockClient((request) async { 22 | expect(request.method, 'POST'); 23 | expect(request.url.toString(), 'https://api.openai.com/v1/chat/completions'); 24 | var chatRequest = ChatRequest.fromJson(jsonDecode(request.body)); 25 | expect(request.headers['Authorization'], 'Bearer deadbeef'); 26 | expect(chatRequest.model, 'gpt-3.5-turbo-0301'); 27 | expect(chatRequest.messages.length, 2); 28 | var chatResponse = { 29 | "id": "chatcmpl-123", 30 | "object": "chat.completion", 31 | "created": 1677652288, 32 | "choices": [{ 33 | "index": 0, 34 | "message": { 35 | "role": "assistant", 36 | "content": "你好", 37 | }, 38 | "finish_reason": "stop" 39 | }], 40 | "usage": { 41 | "prompt_tokens": 9, 42 | "completion_tokens": 12, 43 | "total_tokens": 21 44 | } 45 | }; 46 | return Response(jsonEncode(chatResponse), 200, headers: { 47 | HttpHeaders.contentTypeHeader: 'application/json; charset=utf-8', 48 | }); 49 | }); 50 | 51 | var api = OpenAiApi(SafeHttpClient(mockHTTPClient)); 52 | var messages = [ 53 | ChatMessage('system', 'A helpful assistant to translate from English to Chinese.'), 54 | ChatMessage('user', 'translate: hello') 55 | ]; 56 | var chatResponse = await api.chatCompletion(messages); 57 | expect(chatResponse.choices[0].message.content, isA()); 58 | }); 59 | }); 60 | } --------------------------------------------------------------------------------