feat: model and user avatars
This commit is contained in:
@@ -14,6 +14,7 @@ import '../../../core/auth/auth_state_manager.dart';
|
||||
import '../providers/chat_providers.dart';
|
||||
import '../../../core/utils/debug_logger.dart';
|
||||
import '../../../core/utils/user_display_name.dart';
|
||||
import '../../../core/utils/model_icon_utils.dart';
|
||||
|
||||
import '../widgets/modern_chat_input.dart';
|
||||
import '../widgets/user_message_bubble.dart';
|
||||
@@ -41,6 +42,7 @@ import '../../../shared/widgets/middle_ellipsis_text.dart';
|
||||
import '../../../shared/widgets/modal_safe_area.dart';
|
||||
import '../../../core/services/settings_service.dart';
|
||||
import '../../../shared/utils/conversation_context_menu.dart';
|
||||
import '../../../shared/widgets/model_avatar.dart';
|
||||
// Removed unused PlatformUtils import
|
||||
import '../../../core/services/platform_service.dart' as ps;
|
||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
||||
@@ -699,6 +701,8 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
return _buildEmptyState(Theme.of(context));
|
||||
}
|
||||
|
||||
final apiService = ref.watch(apiServiceProvider);
|
||||
|
||||
return OptimizedList<ChatMessage>(
|
||||
key: const ValueKey('actual_messages'),
|
||||
scrollController: _scrollController,
|
||||
@@ -717,6 +721,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
|
||||
// Resolve a friendly model display name for message headers
|
||||
String? displayModelName;
|
||||
Model? matchedModel;
|
||||
final rawModel = message.model;
|
||||
if (rawModel != null && rawModel.isNotEmpty) {
|
||||
final omitProvider = ref
|
||||
@@ -730,6 +735,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
final match = models.firstWhere(
|
||||
(m) => m.id == rawModel || m.name == rawModel,
|
||||
);
|
||||
matchedModel = match;
|
||||
displayModelName = _formatModelDisplayName(
|
||||
match.name,
|
||||
omitProvider: omitProvider,
|
||||
@@ -750,6 +756,11 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
}
|
||||
}
|
||||
|
||||
final modelIconUrl = resolveModelIconUrlForModel(
|
||||
apiService,
|
||||
matchedModel,
|
||||
);
|
||||
|
||||
// Wrap message in selection container if in selection mode
|
||||
Widget messageWidget;
|
||||
|
||||
@@ -770,6 +781,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
message: message,
|
||||
isStreaming: isStreaming,
|
||||
modelName: displayModelName,
|
||||
modelIconUrl: modelIconUrl,
|
||||
onCopy: () => _copyMessage(message.content),
|
||||
onRegenerate: () => _regenerateMessage(message),
|
||||
);
|
||||
@@ -917,7 +929,6 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
// Use select to watch only connectivity status to reduce rebuilds
|
||||
final isOnline = ref.watch(isOnlineProvider.select((status) => status));
|
||||
|
||||
// Use select to watch only the selected model to reduce rebuilds
|
||||
final selectedModel = ref.watch(
|
||||
selectedModelProvider.select((model) => model),
|
||||
@@ -1819,6 +1830,8 @@ class _ModelSelectorSheetState extends ConsumerState<_ModelSelectorSheet> {
|
||||
required bool isSelected,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
final api = ref.watch(apiServiceProvider);
|
||||
final iconUrl = resolveModelIconUrlForModel(api, model);
|
||||
return PressableScale(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||
@@ -1852,21 +1865,7 @@ class _ModelSelectorSheetState extends ConsumerState<_ModelSelectorSheet> {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: context.conduitTheme.buttonPrimary.withValues(
|
||||
alpha: 0.15,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||
),
|
||||
child: Icon(
|
||||
Platform.isIOS ? CupertinoIcons.cube : Icons.psychology,
|
||||
color: context.conduitTheme.buttonPrimary,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
ModelAvatar(size: 32, imageUrl: iconUrl, label: model.name),
|
||||
const SizedBox(width: Spacing.md),
|
||||
Expanded(
|
||||
child: Column(
|
||||
|
||||
@@ -14,11 +14,13 @@ import 'enhanced_image_attachment.dart';
|
||||
import 'package:conduit/l10n/app_localizations.dart';
|
||||
import 'enhanced_attachment.dart';
|
||||
import 'package:conduit/shared/widgets/chat_action_button.dart';
|
||||
import '../../../shared/widgets/model_avatar.dart';
|
||||
|
||||
class AssistantMessageWidget extends ConsumerStatefulWidget {
|
||||
final dynamic message;
|
||||
final bool isStreaming;
|
||||
final String? modelName;
|
||||
final String? modelIconUrl;
|
||||
final VoidCallback? onCopy;
|
||||
final VoidCallback? onRegenerate;
|
||||
final VoidCallback? onLike;
|
||||
@@ -29,6 +31,7 @@ class AssistantMessageWidget extends ConsumerStatefulWidget {
|
||||
required this.message,
|
||||
this.isStreaming = false,
|
||||
this.modelName,
|
||||
this.modelIconUrl,
|
||||
this.onCopy,
|
||||
this.onRegenerate,
|
||||
this.onLike,
|
||||
@@ -87,8 +90,9 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
||||
_updateTypingIndicatorGate();
|
||||
}
|
||||
|
||||
// Rebuild cached avatar if model name changes
|
||||
if (oldWidget.modelName != widget.modelName) {
|
||||
// Rebuild cached avatar if model name or icon changes
|
||||
if (oldWidget.modelName != widget.modelName ||
|
||||
oldWidget.modelIconUrl != widget.modelIconUrl) {
|
||||
_buildCachedAvatar();
|
||||
}
|
||||
}
|
||||
@@ -409,28 +413,36 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
||||
}
|
||||
|
||||
void _buildCachedAvatar() {
|
||||
_cachedAvatar = Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
final theme = context.conduitTheme;
|
||||
final iconUrl = widget.modelIconUrl?.trim();
|
||||
final hasIcon = iconUrl != null && iconUrl.isNotEmpty;
|
||||
|
||||
final Widget leading = hasIcon
|
||||
? ModelAvatar(size: 20, imageUrl: iconUrl, label: widget.modelName)
|
||||
: Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: context.conduitTheme.buttonPrimary,
|
||||
color: theme.buttonPrimary,
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.small),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.auto_awesome,
|
||||
color: context.conduitTheme.buttonPrimaryText,
|
||||
color: theme.buttonPrimaryText,
|
||||
size: 12,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
_cachedAvatar = Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
leading,
|
||||
const SizedBox(width: Spacing.xs),
|
||||
Text(
|
||||
widget.modelName ?? 'Assistant',
|
||||
style: TextStyle(
|
||||
color: context.conduitTheme.textSecondary,
|
||||
color: theme.textSecondary,
|
||||
fontSize: AppTypography.bodySmall,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: 0.1,
|
||||
|
||||
Reference in New Issue
Block a user