feat: comprehensive reviewer mode
This commit is contained in:
@@ -10,6 +10,7 @@ import '../../../core/providers/app_providers.dart';
|
||||
import '../../../core/auth/auth_state_manager.dart';
|
||||
import '../../../core/utils/stream_chunker.dart';
|
||||
import '../../../core/services/persistent_streaming_service.dart';
|
||||
import '../services/reviewer_mode_service.dart';
|
||||
|
||||
// Chat messages for current conversation
|
||||
final chatMessagesProvider =
|
||||
@@ -399,9 +400,13 @@ Future<void> regenerateMessage(
|
||||
);
|
||||
ref.read(chatMessagesProvider.notifier).addMessage(assistantMessage);
|
||||
|
||||
// Use canned response for regeneration
|
||||
final responseText = ReviewerModeService.generateResponse(
|
||||
userMessage: userMessageContent,
|
||||
);
|
||||
|
||||
// Simulate streaming response
|
||||
final demoText = 'This is a regenerated demo response.\n\nOriginal message: "$userMessageContent"';
|
||||
final words = demoText.split(' ');
|
||||
final words = responseText.split(' ');
|
||||
for (final word in words) {
|
||||
await Future.delayed(const Duration(milliseconds: 40));
|
||||
ref.read(chatMessagesProvider.notifier).appendToLastMessage('$word ');
|
||||
@@ -444,7 +449,7 @@ Future<void> regenerateMessage(
|
||||
}
|
||||
|
||||
// Stream response using SSE
|
||||
final response = await api!.sendMessage(
|
||||
final response = api!.sendMessage(
|
||||
messages: conversationMessages,
|
||||
model: selectedModel.id,
|
||||
conversationId: activeConversation.id,
|
||||
@@ -582,7 +587,10 @@ Future<void> _sendMessageInternal(
|
||||
);
|
||||
|
||||
// Invalidate conversations provider to refresh the list
|
||||
ref.invalidate(conversationsProvider);
|
||||
// Adding a small delay to prevent rapid invalidations that could cause duplicates
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
ref.invalidate(conversationsProvider);
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'DEBUG: Failed to create conversation on server, using local: $e',
|
||||
@@ -615,10 +623,27 @@ Future<void> _sendMessageInternal(
|
||||
);
|
||||
ref.read(chatMessagesProvider.notifier).addMessage(assistantMessage);
|
||||
|
||||
// Check if there are attachments
|
||||
String? filename;
|
||||
if (attachments != null && attachments.isNotEmpty) {
|
||||
// Get the first attachment filename for the response
|
||||
// In reviewer mode, we just simulate having a file
|
||||
filename = "demo_file.txt";
|
||||
}
|
||||
|
||||
// Check if this is voice input
|
||||
// In reviewer mode, we don't have actual voice input state
|
||||
final isVoiceInput = false;
|
||||
|
||||
// Generate appropriate canned response
|
||||
final responseText = ReviewerModeService.generateResponse(
|
||||
userMessage: message,
|
||||
filename: filename,
|
||||
isVoiceInput: isVoiceInput,
|
||||
);
|
||||
|
||||
// Simulate token-by-token streaming
|
||||
final demoText =
|
||||
'This is a demo response from Conduit.\n\nYou typed: "$message"';
|
||||
final words = demoText.split(' ');
|
||||
final words = responseText.split(' ');
|
||||
for (final word in words) {
|
||||
await Future.delayed(const Duration(milliseconds: 40));
|
||||
ref.read(chatMessagesProvider.notifier).appendToLastMessage('$word ');
|
||||
@@ -1598,8 +1623,11 @@ Future<void> _saveConversationToServer(dynamic ref) async {
|
||||
debugPrint(
|
||||
'DEBUG: Invalidating conversations provider after successful save',
|
||||
);
|
||||
ref.invalidate(conversationsProvider);
|
||||
debugPrint('DEBUG: Conversations provider invalidated');
|
||||
// Adding a small delay to prevent rapid invalidations that could cause duplicates
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
ref.invalidate(conversationsProvider);
|
||||
debugPrint('DEBUG: Conversations provider invalidated');
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint('Error saving conversation to server: $e');
|
||||
// Fallback to local storage
|
||||
|
||||
@@ -383,8 +383,132 @@ class FileUploadState {
|
||||
|
||||
enum FileUploadStatus { pending, uploading, completed, failed }
|
||||
|
||||
// Mock file attachment service for reviewer mode
|
||||
class MockFileAttachmentService {
|
||||
final ImagePicker _imagePicker = ImagePicker();
|
||||
|
||||
// Reuse the same methods from parent class
|
||||
Future<List<File>> pickFiles({
|
||||
bool allowMultiple = true,
|
||||
List<String>? allowedExtensions,
|
||||
}) async {
|
||||
try {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
allowMultiple: allowMultiple,
|
||||
type: allowedExtensions != null ? FileType.custom : FileType.any,
|
||||
allowedExtensions: allowedExtensions,
|
||||
);
|
||||
|
||||
if (result == null || result.files.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return result.files
|
||||
.where((file) => file.path != null)
|
||||
.map((file) => File(file.path!))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
throw Exception('Failed to pick files: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<File?> pickImage() async {
|
||||
try {
|
||||
final XFile? image = await _imagePicker.pickImage(
|
||||
source: ImageSource.gallery,
|
||||
imageQuality: 85,
|
||||
);
|
||||
return image != null ? File(image.path) : null;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to pick image: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<File?> takePhoto() async {
|
||||
try {
|
||||
final XFile? photo = await _imagePicker.pickImage(
|
||||
source: ImageSource.camera,
|
||||
imageQuality: 85,
|
||||
);
|
||||
return photo != null ? File(photo.path) : null;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to take photo: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Mock upload file with progress tracking
|
||||
Stream<FileUploadState> uploadFile(File file) async* {
|
||||
foundation.debugPrint('DEBUG: Mock file upload for: ${file.path}');
|
||||
|
||||
final fileName = path.basename(file.path);
|
||||
final fileSize = await file.length();
|
||||
|
||||
// Yield initial state
|
||||
yield FileUploadState(
|
||||
file: file,
|
||||
fileName: fileName,
|
||||
fileSize: fileSize,
|
||||
progress: 0.0,
|
||||
status: FileUploadStatus.uploading,
|
||||
);
|
||||
|
||||
// Simulate upload progress
|
||||
for (int i = 1; i <= 10; i++) {
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
yield FileUploadState(
|
||||
file: file,
|
||||
fileName: fileName,
|
||||
fileSize: fileSize,
|
||||
progress: i / 10,
|
||||
status: FileUploadStatus.uploading,
|
||||
);
|
||||
}
|
||||
|
||||
// Yield completed state with mock file ID
|
||||
yield FileUploadState(
|
||||
file: file,
|
||||
fileName: fileName,
|
||||
fileSize: fileSize,
|
||||
progress: 1.0,
|
||||
status: FileUploadStatus.completed,
|
||||
fileId: 'mock_file_${DateTime.now().millisecondsSinceEpoch}',
|
||||
);
|
||||
|
||||
foundation.debugPrint('DEBUG: Mock file upload completed');
|
||||
}
|
||||
|
||||
Future<List<String>> uploadFiles(
|
||||
List<File> files, {
|
||||
Function(int, int)? onProgress,
|
||||
required String conversationId,
|
||||
}) async {
|
||||
// Simulate upload progress for reviewer mode
|
||||
final uploadIds = <String>[];
|
||||
|
||||
for (int i = 0; i < files.length; i++) {
|
||||
if (onProgress != null) {
|
||||
// Simulate progress
|
||||
for (int j = 0; j <= 100; j += 10) {
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
onProgress(i, j);
|
||||
}
|
||||
}
|
||||
// Generate mock upload ID
|
||||
uploadIds.add('mock_upload_${DateTime.now().millisecondsSinceEpoch}_$i');
|
||||
}
|
||||
|
||||
return uploadIds;
|
||||
}
|
||||
}
|
||||
|
||||
// Providers
|
||||
final fileAttachmentServiceProvider = Provider<FileAttachmentService?>((ref) {
|
||||
final fileAttachmentServiceProvider = Provider<dynamic>((ref) {
|
||||
final isReviewerMode = ref.watch(reviewerModeProvider);
|
||||
|
||||
if (isReviewerMode) {
|
||||
return MockFileAttachmentService();
|
||||
}
|
||||
|
||||
final apiService = ref.watch(apiServiceProvider);
|
||||
if (apiService == null) return null;
|
||||
return FileAttachmentService(apiService);
|
||||
|
||||
95
lib/features/chat/services/reviewer_mode_service.dart
Normal file
95
lib/features/chat/services/reviewer_mode_service.dart
Normal file
@@ -0,0 +1,95 @@
|
||||
import 'dart:math';
|
||||
|
||||
class ReviewerModeService {
|
||||
static final Random _random = Random();
|
||||
|
||||
// Categories of canned responses
|
||||
static const Map<String, List<String>> _cannedResponses = {
|
||||
'greeting': [
|
||||
'Hello! I\'m here to help you explore Conduit\'s features. What would you like to know?',
|
||||
'Hi there! Welcome to Conduit. How can I assist you today?',
|
||||
'Greetings! I\'m ready to help you test out Conduit\'s chat capabilities.',
|
||||
],
|
||||
'code': [
|
||||
'Here\'s a simple example of what I can help with:\n\n```python\ndef greet(name):\n return f"Hello, {name}!"\n\nprint(greet("Conduit User"))\n```\n\nI can assist with various programming languages and tasks.',
|
||||
'I can help you write and review code. For example:\n\n```javascript\nconst calculateSum = (numbers) => {\n return numbers.reduce((acc, num) => acc + num, 0);\n};\n\nconsole.log(calculateSum([1, 2, 3, 4, 5])); // Output: 15\n```',
|
||||
'Let me show you a code snippet:\n\n```typescript\ninterface User {\n id: string;\n name: string;\n email: string;\n}\n\nclass UserService {\n async getUser(id: string): Promise<User> {\n // Implementation here\n return { id, name: "Demo User", email: "demo@conduit.app" };\n }\n}\n```',
|
||||
],
|
||||
'features': [
|
||||
'Conduit offers several key features:\n\n• **Real-time streaming** - See responses as they\'re generated\n• **File attachments** - Share images and documents\n• **Voice input** - Speak your queries\n• **Multiple models** - Choose from various AI models\n• **Conversation history** - Access your past chats\n\nWhat feature would you like to explore?',
|
||||
'Here are some things you can do with Conduit:\n\n1. **Chat with AI** - Have natural conversations\n2. **Share files** - Upload images and documents for analysis\n3. **Use voice** - Tap the microphone for hands-free input\n4. **Switch models** - Try different AI models for varied responses\n5. **Search history** - Find past conversations easily\n\nWhich capability interests you most?',
|
||||
],
|
||||
'attachments': [
|
||||
'I see you\'ve shared a file! In Conduit, I can analyze:\n\n• **Images** - Describe, analyze, or answer questions about pictures\n• **Documents** - Review and summarize text files\n• **Code files** - Help debug or explain code\n\nThe file "{filename}" has been received. What would you like me to do with it?',
|
||||
'Thank you for sharing "{filename}"! I can help you:\n\n• Extract information\n• Analyze content\n• Answer questions about it\n• Provide summaries\n\nWhat specific aspect would you like me to focus on?',
|
||||
],
|
||||
'voice': [
|
||||
'Great! You\'re using voice input. This feature allows for hands-free interaction with Conduit. I heard: "{transcript}"\n\nVoice input is perfect for:\n• Quick queries\n• Accessibility\n• Multitasking\n\nHow else can I help you?',
|
||||
'I received your voice message: "{transcript}"\n\nVoice input makes conversations more natural and convenient. Feel free to continue speaking or typing - whatever works best for you!',
|
||||
],
|
||||
'general': [
|
||||
'That\'s an interesting question! Let me think about "{query}".\n\nIn Conduit, you can explore various topics and get detailed responses. The app is designed to be your AI companion for learning, creating, and problem-solving.\n\n(Demo Mode: These are sample responses for app review)',
|
||||
'Regarding "{query}", here\'s what I can share:\n\nConduit provides a seamless chat experience with advanced AI capabilities. Whether you\'re looking for information, creative assistance, or technical help, I\'m here to support you.\n\nNote: This is a demo response - actual usage requires your own AI server.',
|
||||
'I understand you\'re asking about "{query}". \n\nThis demo shows how Conduit handles conversations. In real use, you\'d connect to your own AI server for actual AI responses.\n\nTry uploading an image or using voice input to see more features!',
|
||||
],
|
||||
'error': [
|
||||
'I noticed there might be an issue. In a production environment, Conduit handles errors gracefully and provides helpful feedback. This demo mode simulates that experience.\n\nPlease try your request again, or let me know how I can help differently!',
|
||||
'It seems something unexpected happened. Conduit is designed to recover smoothly from errors and continue providing assistance.\n\nWould you like to try a different query or explore another feature?',
|
||||
],
|
||||
};
|
||||
|
||||
static String generateResponse({
|
||||
required String userMessage,
|
||||
String? filename,
|
||||
bool isVoiceInput = false,
|
||||
}) {
|
||||
final lowerMessage = userMessage.toLowerCase();
|
||||
|
||||
// Determine response category
|
||||
String category = 'general';
|
||||
|
||||
if (lowerMessage.contains('hello') ||
|
||||
lowerMessage.contains('hi') ||
|
||||
lowerMessage.contains('hey') ||
|
||||
lowerMessage.contains('greet')) {
|
||||
category = 'greeting';
|
||||
} else if (lowerMessage.contains('code') ||
|
||||
lowerMessage.contains('program') ||
|
||||
lowerMessage.contains('function') ||
|
||||
lowerMessage.contains('debug')) {
|
||||
category = 'code';
|
||||
} else if (lowerMessage.contains('feature') ||
|
||||
lowerMessage.contains('capability') ||
|
||||
lowerMessage.contains('what can') ||
|
||||
lowerMessage.contains('help')) {
|
||||
category = 'features';
|
||||
} else if (filename != null) {
|
||||
category = 'attachments';
|
||||
} else if (isVoiceInput) {
|
||||
category = 'voice';
|
||||
}
|
||||
|
||||
// Get responses for category
|
||||
final responses = _cannedResponses[category] ?? _cannedResponses['general']!;
|
||||
final response = responses[_random.nextInt(responses.length)];
|
||||
|
||||
// Replace placeholders
|
||||
return response
|
||||
.replaceAll('{query}', userMessage)
|
||||
.replaceAll('{filename}', filename ?? 'file')
|
||||
.replaceAll('{transcript}', userMessage);
|
||||
}
|
||||
|
||||
static String generateStreamingResponse({
|
||||
required String userMessage,
|
||||
String? filename,
|
||||
bool isVoiceInput = false,
|
||||
}) {
|
||||
// For streaming, we'll return the same response but the UI will handle chunking
|
||||
return generateResponse(
|
||||
userMessage: userMessage,
|
||||
filename: filename,
|
||||
isVoiceInput: isVoiceInput,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import '../../../core/models/model.dart';
|
||||
import '../../../shared/widgets/loading_states.dart';
|
||||
import 'chat_page_helpers.dart';
|
||||
import '../../../shared/widgets/themed_dialogs.dart';
|
||||
import '../../onboarding/views/onboarding_sheet.dart';
|
||||
|
||||
class ChatPage extends ConsumerStatefulWidget {
|
||||
const ChatPage({super.key});
|
||||
@@ -58,12 +59,172 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
return display;
|
||||
}
|
||||
|
||||
bool validateFileCount(int currentCount, int newCount, int maxCount) {
|
||||
return (currentCount + newCount) <= maxCount;
|
||||
}
|
||||
|
||||
bool validateFileSize(int fileSize, int maxSizeMB) {
|
||||
return fileSize <= (maxSizeMB * 1024 * 1024);
|
||||
}
|
||||
|
||||
void startNewChat() {
|
||||
// Clear current conversation
|
||||
ref.read(chatMessagesProvider.notifier).clearMessages();
|
||||
ref.read(activeConversationProvider.notifier).state = null;
|
||||
|
||||
// Scroll to top
|
||||
if (_scrollController.hasClients) {
|
||||
_scrollController.jumpTo(0);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _checkAndAutoSelectModel() async {
|
||||
// Check if a model is already selected
|
||||
final selectedModel = ref.read(selectedModelProvider);
|
||||
if (selectedModel != null) {
|
||||
debugPrint('DEBUG: Model already selected: ${selectedModel.name}');
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('DEBUG: No model selected, attempting auto-selection');
|
||||
|
||||
try {
|
||||
// First ensure models are loaded
|
||||
final modelsAsync = ref.read(modelsProvider);
|
||||
List<Model> models;
|
||||
|
||||
if (modelsAsync.hasValue) {
|
||||
models = modelsAsync.value!;
|
||||
} else {
|
||||
debugPrint('DEBUG: Models not loaded yet, fetching...');
|
||||
models = await ref.read(modelsProvider.future);
|
||||
}
|
||||
|
||||
debugPrint('DEBUG: Found ${models.length} models available');
|
||||
|
||||
if (models.isEmpty) {
|
||||
debugPrint('DEBUG: No models available for selection');
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to use the default model provider
|
||||
try {
|
||||
final model = await ref.read(defaultModelProvider.future);
|
||||
if (model != null) {
|
||||
debugPrint('DEBUG: Model auto-selected via provider: ${model.name}');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('DEBUG: Default provider failed, selecting first model directly');
|
||||
// Fallback: select the first available model
|
||||
ref.read(selectedModelProvider.notifier).state = models.first;
|
||||
debugPrint('DEBUG: Fallback model selected: ${models.first.name}');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('DEBUG: Failed to auto-select model: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _checkAndShowOnboarding() async {
|
||||
try {
|
||||
// Check if onboarding has been seen
|
||||
final storage = ref.read(optimizedStorageServiceProvider);
|
||||
final seen = await storage.getOnboardingSeen();
|
||||
debugPrint('DEBUG: Chat page - Onboarding seen status: $seen');
|
||||
|
||||
if (!seen && mounted) {
|
||||
// Small delay to ensure navigation has settled
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
if (!mounted) return;
|
||||
|
||||
debugPrint('DEBUG: Showing onboarding from chat page');
|
||||
_showOnboarding();
|
||||
await storage.setOnboardingSeen(true);
|
||||
debugPrint('DEBUG: Onboarding marked as seen');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('DEBUG: Error checking onboarding status: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _showOnboarding() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: context.conduitTheme.surfaceBackground,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(AppBorderRadius.modal),
|
||||
),
|
||||
boxShadow: ConduitShadows.modal,
|
||||
),
|
||||
child: const OnboardingSheet(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _checkAndLoadDemoConversation() async {
|
||||
final isReviewerMode = ref.read(reviewerModeProvider);
|
||||
if (!isReviewerMode) return;
|
||||
|
||||
// Check if there's already an active conversation
|
||||
final activeConversation = ref.read(activeConversationProvider);
|
||||
if (activeConversation != null) {
|
||||
debugPrint('Conversation already active: ${activeConversation.title}');
|
||||
return;
|
||||
}
|
||||
|
||||
// Force refresh conversations provider to ensure we get the demo conversations
|
||||
ref.invalidate(conversationsProvider);
|
||||
|
||||
// Try to load demo conversation
|
||||
for (int i = 0; i < 10; i++) {
|
||||
final conversationsAsync = ref.read(conversationsProvider);
|
||||
|
||||
if (conversationsAsync.hasValue && conversationsAsync.value!.isNotEmpty) {
|
||||
// Find and load the welcome conversation
|
||||
final welcomeConv = conversationsAsync.value!.firstWhere(
|
||||
(conv) => conv.id == 'demo-conv-1',
|
||||
orElse: () => conversationsAsync.value!.first,
|
||||
);
|
||||
|
||||
ref.read(activeConversationProvider.notifier).state = welcomeConv;
|
||||
debugPrint('Auto-loaded demo conversation: ${welcomeConv.title}');
|
||||
return;
|
||||
}
|
||||
|
||||
// If conversations are still loading, wait a bit and retry
|
||||
if (conversationsAsync.isLoading || i == 0) {
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
continue;
|
||||
}
|
||||
|
||||
// If there was an error or no conversations, break
|
||||
break;
|
||||
}
|
||||
|
||||
debugPrint('Failed to auto-load demo conversation');
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Listen to scroll events to show/hide scroll to bottom button
|
||||
_scrollController.addListener(_onScroll);
|
||||
|
||||
// Initialize chat page components
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
// First, ensure a model is selected
|
||||
await _checkAndAutoSelectModel();
|
||||
|
||||
// Then check for demo conversation in reviewer mode
|
||||
await _checkAndLoadDemoConversation();
|
||||
|
||||
// Finally, show onboarding if needed
|
||||
await _checkAndShowOnboarding();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -89,8 +250,9 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
}
|
||||
|
||||
final isOnline = ref.read(isOnlineProvider);
|
||||
debugPrint('DEBUG: Online status: $isOnline');
|
||||
if (!isOnline) {
|
||||
final isReviewerMode = ref.read(reviewerModeProvider);
|
||||
debugPrint('DEBUG: Online status: $isOnline, Reviewer mode: $isReviewerMode');
|
||||
if (!isOnline && !isReviewerMode) {
|
||||
debugPrint('DEBUG: Offline - cannot send message');
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -102,7 +264,6 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
// TODO: Implement message queueing for offline mode
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -402,7 +563,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
|
||||
void _handleNewChat() {
|
||||
// Start a new chat using the existing function
|
||||
startNewChat(ref);
|
||||
startNewChat();
|
||||
|
||||
// Hide scroll-to-bottom button for a fresh chat
|
||||
if (mounted) {
|
||||
@@ -1080,6 +1241,16 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
final selectedModel = ref.watch(
|
||||
selectedModelProvider.select((model) => model),
|
||||
);
|
||||
|
||||
// Watch reviewer mode and auto-select model if needed
|
||||
final isReviewerMode = ref.watch(reviewerModeProvider);
|
||||
|
||||
// Auto-select model when in reviewer mode with no selection
|
||||
if (isReviewerMode && selectedModel == null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_checkAndAutoSelectModel();
|
||||
});
|
||||
}
|
||||
|
||||
return ErrorBoundary(
|
||||
child: PopScope(
|
||||
@@ -1181,45 +1352,76 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
(models) => _showModelDropdown(context, ref, models),
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
_formatModelDisplayName(selectedModel.name),
|
||||
style: AppTypography.headlineSmallStyle.copyWith(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
fontWeight: FontWeight.w400,
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
_formatModelDisplayName(selectedModel.name),
|
||||
style: AppTypography.headlineSmallStyle.copyWith(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(width: Spacing.xs),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.xs,
|
||||
vertical: Spacing.xxs,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: context.conduitTheme.surfaceBackground
|
||||
.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppBorderRadius.badge,
|
||||
),
|
||||
border: Border.all(
|
||||
color: context.conduitTheme.dividerColor,
|
||||
width: BorderWidth.thin,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Platform.isIOS
|
||||
? CupertinoIcons.chevron_down
|
||||
: Icons.keyboard_arrow_down,
|
||||
color: context.conduitTheme.iconSecondary,
|
||||
size: IconSize.small,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: Spacing.xs),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.xs,
|
||||
vertical: Spacing.xxs,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: context.conduitTheme.surfaceBackground
|
||||
.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppBorderRadius.badge,
|
||||
),
|
||||
border: Border.all(
|
||||
color: context.conduitTheme.dividerColor,
|
||||
width: BorderWidth.thin,
|
||||
if (ref.watch(reviewerModeProvider))
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.sm,
|
||||
vertical: 1.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: context.conduitTheme.success.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.badge),
|
||||
border: Border.all(
|
||||
color: context.conduitTheme.success.withValues(alpha: 0.3),
|
||||
width: BorderWidth.thin,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'REVIEWER MODE',
|
||||
style: AppTypography.captionStyle.copyWith(
|
||||
color: context.conduitTheme.success,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 9,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Platform.isIOS
|
||||
? CupertinoIcons.chevron_down
|
||||
: Icons.keyboard_arrow_down,
|
||||
color: context.conduitTheme.iconSecondary,
|
||||
size: IconSize.small,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
@@ -1230,47 +1432,78 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
(models) => _showModelDropdown(context, ref, models),
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
'Choose Model',
|
||||
style: AppTypography.headlineSmallStyle.copyWith(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
fontWeight: FontWeight.w400,
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
'Choose Model',
|
||||
style: AppTypography.headlineSmallStyle.copyWith(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(width: Spacing.xs),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.xs,
|
||||
vertical: Spacing.xxs,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: context.conduitTheme.surfaceBackground
|
||||
.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppBorderRadius.badge,
|
||||
),
|
||||
border: Border.all(
|
||||
color: context.conduitTheme.dividerColor,
|
||||
width: BorderWidth.thin,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Platform.isIOS
|
||||
? CupertinoIcons.chevron_down
|
||||
: Icons.keyboard_arrow_down,
|
||||
color: context.conduitTheme.iconSecondary,
|
||||
size: IconSize.small,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: Spacing.xs),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.xs,
|
||||
vertical: Spacing.xxs,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: context.conduitTheme.surfaceBackground
|
||||
.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppBorderRadius.badge,
|
||||
),
|
||||
border: Border.all(
|
||||
color: context.conduitTheme.dividerColor,
|
||||
width: BorderWidth.thin,
|
||||
if (ref.watch(reviewerModeProvider))
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.sm,
|
||||
vertical: 1.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: context.conduitTheme.success.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.badge),
|
||||
border: Border.all(
|
||||
color: context.conduitTheme.success.withValues(alpha: 0.3),
|
||||
width: BorderWidth.thin,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'REVIEWER MODE',
|
||||
style: AppTypography.captionStyle.copyWith(
|
||||
color: context.conduitTheme.success,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 9,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Platform.isIOS
|
||||
? CupertinoIcons.chevron_down
|
||||
: Icons.keyboard_arrow_down,
|
||||
color: context.conduitTheme.iconSecondary,
|
||||
size: IconSize.small,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1344,7 +1577,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
|
||||
// Modern Input (root matches input background including safe area)
|
||||
ModernChatInput(
|
||||
enabled: selectedModel != null && isOnline,
|
||||
enabled: selectedModel != null && (isOnline || ref.watch(reviewerModeProvider)),
|
||||
onSendMessage: (text) =>
|
||||
_handleMessageSend(text, selectedModel),
|
||||
onVoiceInput: _handleVoiceInput,
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'dart:io';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import '../../../shared/theme/app_theme.dart';
|
||||
import '../../../shared/theme/theme_extensions.dart';
|
||||
import '../../../shared/widgets/conduit_components.dart';
|
||||
import '../../../shared/utils/ui_utils.dart';
|
||||
|
||||
Reference in New Issue
Block a user