Skip to content

Commit

Permalink
feat: Added image search feature
Browse files Browse the repository at this point in the history
    1. Add image search feature
    2. Used native package provided by google and removed third party package of generative ai
  • Loading branch information
vineyrawat committed Apr 21, 2024
1 parent 8bf9983 commit d7eec91
Show file tree
Hide file tree
Showing 11 changed files with 399 additions and 152 deletions.
15 changes: 8 additions & 7 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<application
android:label="convogen"
android:name="${applicationName}"
Expand All @@ -17,12 +18,12 @@
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
Expand All @@ -31,4 +32,4 @@
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
</manifest>
70 changes: 57 additions & 13 deletions lib/providers/gemini_chat_provider.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_gemini/flutter_gemini.dart';
// import 'package:flutter_gemini/flutter_gemini.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:convogen/providers/app_settings_provider.dart';
import 'package:google_generative_ai/google_generative_ai.dart';
import 'package:image_picker/image_picker.dart';

var geminiChatProvider =
StateNotifierProvider<GeminiChatProvider, GeminiChatState>(
Expand Down Expand Up @@ -74,27 +77,68 @@ class GeminiChatProvider extends StateNotifier<GeminiChatState> {
state = state.copyWith(messages: messages);
}

getFromText(String prompt) async {
getPrompt(String prompt, XFile? result) async {
var model = GenerativeModel(
model: 'gemini-pro',
apiKey: ref.read(appSettingsProvider).geminiApiKey);
var filteredMessage = state.messages.whereType<types.TextMessage>();
// return;
var history = filteredMessage
.map((dynamic e) => e.author == state.users[1]
? Content.model([TextPart(e.text)])
: Content.text(e.text))
.toList()
.reversed
.toList();

var chat = model.startChat(history: history);
if (result != null) {
final bytes = await result.readAsBytes();
final image = await decodeImageFromList(bytes);
addMessage(types.ImageMessage(
name: result.name,
size: bytes.length,
author: state.users[0],
id: DateTime.now().toString(),
uri: result.path,
height: image.height.toDouble(),
width: image.width.toDouble(),
createdAt: DateTime.now().millisecondsSinceEpoch,
));
}

addMessage(types.TextMessage(
author: state.users[0],
id: DateTime.now().toString(),
text: prompt,
createdAt: DateTime.now().millisecondsSinceEpoch));
state = state.copyWith(isTyping: true);

var chats = state.messages.map((dynamic e) => Content(
parts: [Parts(text: e.text)],
role: e.author == state.users[1] ? 'model' : 'user'));
var flutterGemini =
Gemini.init(apiKey: ref.read(appSettingsProvider).geminiApiKey);
state = state.copyWith(isTyping: true);

var res = await flutterGemini.chat(chats.toList().reversed.toList());
addMessage(types.TextMessage(
try {
if (prompt.isEmpty && result == null) return;
if (result != null) {}
var res = await chat.sendMessage(result == null
? Content.text(prompt)
: Content.multi([
TextPart(prompt),
...[DataPart("image/jpeg", await result.readAsBytes())]
]));
log(res.text!);
addMessage(types.TextMessage(
author: state.users[1],
id: DateTime.now().toString(),
text: res.text ?? "No response",
createdAt: DateTime.now().millisecondsSinceEpoch));
} catch (e) {
addMessage(types.TextMessage(
author: state.users[1],
id: DateTime.now().toString(),
// text: res!.content!.parts!.map((e) => e.text).join("\n"),
text: res?.output ?? "Unable to proceed",
createdAt: DateTime.now().millisecondsSinceEpoch));
text: e.toString(),
createdAt: DateTime.now().millisecondsSinceEpoch,
));
log(e.toString());
}
state = state.copyWith(isTyping: false);
}
}
87 changes: 78 additions & 9 deletions lib/screens/chat.dart
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
import 'dart:developer';
import 'dart:io';

import "package:flutter/material.dart";
import 'package:image_picker/image_picker.dart';
import 'package:loading_animation_widget/loading_animation_widget.dart';
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:convogen/providers/gemini_chat_provider.dart';
import 'package:simple_gradient_text/simple_gradient_text.dart';
import 'package:shimmer/shimmer.dart';

class ChatPage extends ConsumerWidget {
class ChatPage extends ConsumerStatefulWidget {
const ChatPage({super.key});

@override
Widget build(BuildContext context, WidgetRef ref) {
ConsumerState<ChatPage> createState() => _ChatPageState();
}

class _ChatPageState extends ConsumerState<ChatPage> {
XFile? selectedImage;

@override
Widget build(BuildContext context) {
var geminiChat = ref.watch(geminiChatProvider);

if (geminiChat is InitialLoadingState) {
Expand All @@ -26,7 +37,9 @@ class ChatPage extends ConsumerWidget {
},
emptyState: EmptyStateWidget(onSendPressed: (p0) async {
FocusManager.instance.primaryFocus?.unfocus();
await ref.read(geminiChatProvider.notifier).getFromText(p0);
await ref
.read(geminiChatProvider.notifier)
.getPrompt(p0, selectedImage);
}),
theme: Theme.of(context).brightness == Brightness.dark
? DarkChatTheme(
Expand All @@ -45,10 +58,28 @@ class ChatPage extends ConsumerWidget {
),
messages: geminiChat.messages,
onSendPressed: (p0) async {},
customBottomWidget: CustomBottomInputBar(onSendPressed: (p0) async {
FocusManager.instance.primaryFocus?.unfocus();
await ref.read(geminiChatProvider.notifier).getFromText(p0);
}),
customBottomWidget: CustomBottomInputBar(
selectedImage: selectedImage,
setImage: (XFile? image) {
setState(() {
selectedImage = image;
log("SET IMAGE TO: ${image!.path}");
});
},
onSendPressed: (p0) async {
FocusManager.instance.primaryFocus?.unfocus();
if (selectedImage != null) {
var p = selectedImage;
setState(() {
selectedImage = null;
});
await ref.read(geminiChatProvider.notifier).getPrompt(p0, p!);
} else if (p0.isNotEmpty) {
await ref
.read(geminiChatProvider.notifier)
.getPrompt(p0, selectedImage);
}
}),
typingIndicatorOptions: TypingIndicatorOptions(
customTypingIndicator: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
Expand Down Expand Up @@ -202,13 +233,29 @@ class EmptyStateWidget extends StatelessWidget {
class CustomBottomInputBar extends StatelessWidget {
final bool collapsed;
final Function onSendPressed;
final Function setImage;
final XFile? selectedImage;
const CustomBottomInputBar(
{super.key, this.collapsed = false, required this.onSendPressed});
{super.key,
this.selectedImage,
this.collapsed = false,
required this.onSendPressed,
required this.setImage});

@override
Widget build(BuildContext context) {
var inputController = TextEditingController();

handleCameraPressed() {
log("Camera pressed");
ImagePicker().pickImage(source: ImageSource.gallery).then((image) {
if (image != null) {
log("IMAGE SELECTED: ${image.path}");
setImage(image);
}
});
}

return AnimatedContainer(
duration: const Duration(milliseconds: 300),
// height: 100,
Expand Down Expand Up @@ -248,6 +295,28 @@ class CustomBottomInputBar extends StatelessWidget {
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
selectedImage != null
? Container(
margin: const EdgeInsets.only(right: 10),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.file(
File(selectedImage!.path),
width: 50,
height: 50,
),
),
)
: const SizedBox(),
selectedImage != null
? IconButton(
onPressed: () => setImage(null),
icon: Icon(
Icons.delete_outline,
color: Theme.of(context).colorScheme.error,
))
: const SizedBox(),
selectedImage != null ? const Spacer() : const SizedBox(),
FilledButton(
// color: Colors.red,
style: ButtonStyle(
Expand All @@ -265,7 +334,7 @@ class CustomBottomInputBar extends StatelessWidget {
width: 10,
),
IconButton(
onPressed: () {},
onPressed: handleCameraPressed,
icon: const Icon(Icons.camera_alt_outlined))
],
),
Expand Down
8 changes: 4 additions & 4 deletions lib/screens/settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ class SettingsScreen extends ConsumerWidget {
context: context,
type: ToastificationType.success,
style: ToastificationStyle.flat,
title: 'Settings Saved',
description: 'New settings has been saved',
title: const Text('Settings Saved'),
description: const Text('New settings has been saved'),
alignment: Alignment.bottomCenter,
autoCloseDuration: const Duration(seconds: 4),
boxShadow: lowModeShadow,
Expand Down Expand Up @@ -61,8 +61,8 @@ class SettingsScreen extends ConsumerWidget {
context: context,
type: ToastificationType.success,
style: ToastificationStyle.flat,
title: 'Copied',
description: 'API key copied to clipboard',
title: const Text('Copied'),
description: const Text('API key copied to clipboard'),
alignment: Alignment.bottomCenter,
autoCloseDuration: const Duration(seconds: 4),
boxShadow: lowModeShadow,
Expand Down
4 changes: 4 additions & 0 deletions linux/flutter/generated_plugin_registrant.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@

#include "generated_plugin_registrant.h"

#include <file_selector_linux/file_selector_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>

void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
Expand Down
1 change: 1 addition & 0 deletions linux/flutter/generated_plugins.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#

list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
url_launcher_linux
)

Expand Down
2 changes: 2 additions & 0 deletions macos/Flutter/GeneratedPluginRegistrant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
import FlutterMacOS
import Foundation

import file_selector_macos
import path_provider_foundation
import shared_preferences_foundation
import url_launcher_macos

func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
Expand Down
Loading

0 comments on commit d7eec91

Please sign in to comment.