├── .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 | 
8 | 
9 | 
10 | 
11 |
12 | 
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