这是indexloc提供的服务,不要输入任何密码
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
\nflutter/
42 changes: 42 additions & 0 deletions lib/providers/chat_providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,46 @@ class ActiveChatSessionNotifier extends StateNotifier<ChatSession?> {
_ref.read(isLoadingProvider.notifier).state = false;
}
}

Future<void> sendMessageWithImage(
String content,
String base64Image,
) async {
if (state == null) return;

final userMessage = Message(
role: MessageRole.user,
content: content,
);

final updatedSession = state!.addMessage(userMessage);
state = updatedSession;
_ref.read(chatSessionsProvider.notifier).updateSession(updatedSession);

_ref.read(isLoadingProvider.notifier).state = true;

try {
final llmService = _ref.read(selectedLlmServiceProvider);

final assistantMessage = await llmService.sendMessageWithImage(
updatedSession.messages,
base64Image,
);

final finalSession = updatedSession.addMessage(assistantMessage);
state = finalSession;
_ref.read(chatSessionsProvider.notifier).updateSession(finalSession);
} catch (e) {
final errorMessage = Message(
role: MessageRole.assistant,
content: 'Error: ${e.toString()}',
);

final finalSession = updatedSession.addMessage(errorMessage);
state = finalSession;
_ref.read(chatSessionsProvider.notifier).updateSession(finalSession);
} finally {
_ref.read(isLoadingProvider.notifier).state = false;
}
}
}
10 changes: 10 additions & 0 deletions lib/services/azure_openai_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ class AzureOpenAiService implements LlmService {
throw Exception('Error communicating with Azure OpenAI: $e');
}
}

@override
Future<Message> sendMessageWithImage(
List<Message> messages,
String base64Image,
) async {
// Azure OpenAI does not currently support image inputs via this interface.
// Fallback to sending the text-only message.
return sendMessage(messages);
}

@override
Future<List<String>> getAvailableModels() async {
Expand Down
6 changes: 6 additions & 0 deletions lib/services/llm_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ abstract class LlmService {

/// Send a message to the LLM service and get a response
Future<Message> sendMessage(List<Message> messages);

/// Send a message with an attached image encoded as base64
Future<Message> sendMessageWithImage(
List<Message> messages,
String base64Image,
);

/// Get the available models for this service
Future<List<String>> getAvailableModels();
Expand Down
9 changes: 9 additions & 0 deletions lib/services/ollama_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ class OllamaService implements LlmService {
}
}

@override
Future<Message> sendMessageWithImage(
List<Message> messages,
String base64Image,
) async {
// Ollama currently has no direct image input support; ignore the image.
return sendMessage(messages);
}

String _formatMessagesForOllama(List<Message> messages) {
// Simple formatting for Ollama
// This is a basic implementation and might need to be adjusted based on the specific model
Expand Down
61 changes: 61 additions & 0 deletions lib/services/openai_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,67 @@ class OpenAiService implements LlmService {
throw Exception('Error communicating with OpenAI: $e');
}
}

@override
Future<Message> sendMessageWithImage(
List<Message> messages,
String base64Image,
) async {
final apiKey = await _secureStorage.read(key: _apiKeyKey);
if (apiKey == null || apiKey.isEmpty) {
throw Exception('OpenAI API key not configured');
}

final model = await getCurrentModel();

final formattedMessages = messages.map((m) {
final role = m.role.toString().split('.').last;
if (identical(m, messages.last) && m.role == MessageRole.user) {
return {
'role': role,
'content': [
{'type': 'text', 'text': m.content},
{
'type': 'image_url',
'image_url': 'data:image/png;base64,$base64Image'
},
],
};
}
return {
'role': role,
'content': m.content,
};
}).toList();

try {
final response = await _dio.post(
'$_baseUrl/chat/completions',
options: Options(
headers: {
'Authorization': 'Bearer $apiKey',
'Content-Type': 'application/json',
},
),
data: {
'model': model,
'messages': formattedMessages,
},
);

if (response.statusCode == 200) {
final content = response.data['choices'][0]['message']['content'];
return Message(
role: MessageRole.assistant,
content: content,
);
} else {
throw Exception('Failed to get response from OpenAI: ${response.statusCode}');
}
} catch (e) {
throw Exception('Error communicating with OpenAI: $e');
}
}

@override
Future<List<String>> getAvailableModels() async {
Expand Down
45 changes: 41 additions & 4 deletions lib/ui/screens/chat_screen.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import 'dart:convert';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import '../../models/message.dart';
import '../../providers/chat_providers.dart';
import '../widgets/message_bubble.dart';
Expand Down Expand Up @@ -89,6 +93,7 @@ class MessageInput extends ConsumerStatefulWidget {
class _MessageInputState extends ConsumerState<MessageInput> {
final TextEditingController _controller = TextEditingController();
bool _canSend = false;
File? _selectedImage;

@override
void initState() {
Expand All @@ -106,16 +111,40 @@ class _MessageInputState extends ConsumerState<MessageInput> {
void _updateCanSend() {
final text = _controller.text.trim();
setState(() {
_canSend = text.isNotEmpty;
_canSend = text.isNotEmpty || _selectedImage != null;
});
}

void _sendMessage() {
Future<void> _pickImage() async {
final picker = ImagePicker();
final picked = await picker.pickImage(source: ImageSource.gallery);
if (picked != null) {
setState(() {
_selectedImage = File(picked.path);
_updateCanSend();
});
}
}

Future<void> _sendMessage() async {
final text = _controller.text.trim();
if (text.isEmpty) return;
if (text.isEmpty && _selectedImage == null) return;

if (_selectedImage != null) {
final bytes = await _selectedImage!.readAsBytes();
final base64Image = base64Encode(bytes);
await ref
.read(activeChatSessionProvider.notifier)
.sendMessageWithImage(text, base64Image);
} else {
await ref.read(activeChatSessionProvider.notifier).sendMessage(text);
}

ref.read(activeChatSessionProvider.notifier).sendMessage(text);
_controller.clear();
setState(() {
_selectedImage = null;
_updateCanSend();
});
}

@override
Expand Down Expand Up @@ -152,6 +181,14 @@ class _MessageInputState extends ConsumerState<MessageInput> {
),
),
const SizedBox(width: 8.0),
IconButton(
icon: Icon(
_selectedImage == null ? Icons.attach_file : Icons.check,
),
onPressed: isLoading ? null : _pickImage,
color: Theme.of(context).colorScheme.secondary,
),
const SizedBox(width: 8.0),
FutureBuilder<bool>(
future: isConfigured,
builder: (context, snapshot) {
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,10 +5,12 @@
import FlutterMacOS
import Foundation

import file_selector_macos
import flutter_secure_storage_macos
import path_provider_foundation

func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
}
Loading