using ChatApp.Models;
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace ChatApp.ViewModel
{
public class ChatViewModel : INotifyPropertyChanged
{
public ObservableCollection<MessageModel> Messages { get; set; } = new();
public event Action ScrollToEndRequested; // ✅ Добавлено
public async Task SendMessageAsync(string userInput)
{
if (string.IsNullOrWhiteSpace(userInput)) return;
// 1. Сразу добавляем пользовательское сообщение
Messages.Add(new MessageModel
{
Text = userInput,
IsUser = true,
SenderName = "Вы",
Timestamp = DateTime.Now
});
ScrollToEndRequested?.Invoke();
// 2. Ждём 2 секунды (задержка ответа бота)
await Task.Delay(2000);
// 3. Добавляем сообщение от бота
Messages.Add(new MessageModel
{
Text = await GetGptResponseAsync(userInput),
IsUser = false,
SenderName = "ChatGPT",
Timestamp = DateTime.Now
});
ScrollToEndRequested?.Invoke();
}
private async Task<string> GetGptResponseAsync(string prompt)
{
// Здесь подключается OpenAI SDK или HttpClient
await Task.Delay(500); // эмуляция запроса
return $"🤖 {prompt} — понял!";
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
using System.Globalization;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
namespace ChatApp.Converters
{
public class BoolToCornerRadiusConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
bool isUser = (bool)value;
// CornerRadius: TopLeft, TopRight, BottomRight, BottomLeft
if (isUser)
return new CornerRadius(20, 20, 20, 0); // правая сторона (пользователь)
else
return new CornerRadius(20, 20, 0, 20); // левая сторона (бот)
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
}
using Microsoft.Maui.Controls;
namespace ChatApp.Behaviors
{
public class RemoveUnderlineBehavior : Behavior<VisualElement>
{
protected override void OnAttachedTo(VisualElement bindable)
{
base.OnAttachedTo(bindable);
ApplyRemoveUnderlineStyle(bindable);
}
protected override void OnDetachingFrom(VisualElement bindable)
{
base.OnDetachingFrom(bindable);
}
private void ApplyRemoveUnderlineStyle(VisualElement element)
{
// Установка стиля для Android
#if ANDROID
element.HandlerChanged += (s, e) =>
{
var platformView = element.Handler?.PlatformView;
if (platformView is Android.Widget.EditText editText)
{
editText.Background = null;
}
else if (platformView is Android.Widget.DatePicker datePicker)
{
Android.Graphics.Drawables.LayerDrawable layerDrawable = new Android.Graphics.Drawables.LayerDrawable(new Android.Graphics.Drawables.Drawable[] { });
datePicker.Background = layerDrawable; // Удаление подчеркивания у DatePicker
}
};
#endif
// Установка стиля для iOS
#if IOS
element.HandlerChanged += (s, e) =>
{
if (element.Handler?.PlatformView is UIKit.UITextField textField)
{
textField.BorderStyle = UIKit.UITextBorderStyle.None;
}
else if (element.Handler?.PlatformView is UIKit.UITextView textView)
{
textView.TextContainer.LineFragmentPadding = 0;
textView.TextContainerInset = UIKit.UIEdgeInsets.Zero;
}
};
#endif
}
}
}
using Android.App;
using Android.Content.PM;
using Android.OS;
using Android.Views;
namespace ChatApp
{
[Activity(Theme = "@style/Maui.SplashTheme",
MainLauncher = true,
LaunchMode = LaunchMode.SingleTop,
WindowSoftInputMode = SoftInput.AdjustResize,
ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity
{
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
Window.SetSoftInputMode(Android.Views.SoftInput.AdjustResize);
}
}
}
<Editor
x:Name="UserInput"
AutoSize="TextChanges"
MaximumHeightRequest="150"
Placeholder="Введите сообщение..." />
<ContentPage
x:Class="ChatApp.MainPage"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
BackgroundColor="white">
<Grid RowDefinitions="Auto, *, Auto">
<VerticalStackLayout
Grid.Row="0"
Padding="0,10"
BackgroundColor="White">
<Label
Margin="6"
FontAttributes="Bold"
FontSize="Title"
Text="Chat"
VerticalOptions="Center" />
<BoxView HeightRequest="1" Color="LightGray" />
</VerticalStackLayout>
<!-- История сообщений -->
<CollectionView
x:Name="MessagesView"
Grid.Row="1"
Margin="6,0"
BackgroundColor="White"
ItemsSource="{Binding Messages}"
VerticalOptions="Fill">
<CollectionView.ItemTemplate>
<DataTemplate>
<!-- Если сообщение от пользователя, оно будет в правом столбце, иначе в левом -->
<Grid>
<Grid.Resources>
<Style x:Key="UserGridStyle" TargetType="Grid">
<Setter Property="ColumnDefinitions" Value="Auto,*" />
</Style>
<Style x:Key="BotGridStyle" TargetType="Grid">
<Setter Property="ColumnDefinitions" Value="*,Auto" />
</Style>
</Grid.Resources>
<Grid IsVisible="{Binding IsUser}" Style="{StaticResource UserGridStyle}">
<!-- Время -->
<Label
Grid.Column="{Binding IsUser, Converter={StaticResource ColumnConverter}, ConverterParameter=time}"
Margin="10,0"
FontSize="10"
HorizontalOptions="Start"
Text="{Binding FormattedTime}"
TextColor="Gray"
VerticalOptions="Center" />
<!-- Сообщение -->
<Border
Grid.Column="{Binding IsUser, Converter={StaticResource ColumnConverter}, ConverterParameter=message}"
Margin="4"
Padding="10,6"
BackgroundColor="{Binding IsUser, Converter={StaticResource BoolToColorConverter}}"
HorizontalOptions="{Binding IsUser, Converter={StaticResource MessageAlignmentConverter}}"
Stroke="Transparent">
<Border.StrokeShape>
<RoundRectangle CornerRadius="{Binding IsUser, Converter={StaticResource CornerConverter}}" />
</Border.StrokeShape>
<StackLayout Margin="10,0">
<Label
FontAttributes="Bold"
FontSize="12"
Text="{Binding SenderName}"
TextColor="BlueViolet" />
<Label
FontSize="16"
LineBreakMode="WordWrap"
Text="{Binding Text}" />
</StackLayout>
</Border>
</Grid>
<!-- Grid для BOT -->
<Grid IsVisible="{Binding IsUser, Converter={StaticResource InverseBoolConverter}}" Style="{StaticResource BotGridStyle}">
<Label
Grid.Column="{Binding IsUser, Converter={StaticResource ColumnConverter}, ConverterParameter=time}"
Margin="10,0"
FontSize="10"
HorizontalOptions="Start"
Text="{Binding FormattedTime}"
TextColor="Gray"
VerticalOptions="Center" />
<!-- Сообщение -->
<Border
Grid.Column="{Binding IsUser, Converter={StaticResource ColumnConverter}, ConverterParameter=message}"
Margin="4"
Padding="10,6"
BackgroundColor="{Binding IsUser, Converter={StaticResource BoolToColorConverter}}"
HorizontalOptions="{Binding IsUser, Converter={StaticResource MessageAlignmentConverter}}"
Stroke="Transparent">
<Border.StrokeShape>
<RoundRectangle CornerRadius="{Binding IsUser, Converter={StaticResource CornerConverter}}" />
</Border.StrokeShape>
<StackLayout Margin="10,0">
<Label
FontAttributes="Bold"
FontSize="12"
Text="{Binding SenderName}"
TextColor="BlueViolet" />
<Label
FontSize="16"
LineBreakMode="WordWrap"
Text="{Binding Text}" />
</StackLayout>
</Border>
</Grid>
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<!-- Ввод -->
<VerticalStackLayout
Grid.Row="2"
BackgroundColor="white"
Spacing="6">
<BoxView HeightRequest="1" Color="LightGray" />
<Border
x:Name="InputBorder"
Margin="6"
Background="White"
Stroke="LightGray"
VerticalOptions="Start">
<Border.StrokeShape>
<RoundRectangle CornerRadius="20,20,20,20" />
</Border.StrokeShape>
<Grid Padding="10" ColumnDefinitions="*, Auto">
<Editor
x:Name="UserInput"
AutoSize="TextChanges"
BackgroundColor="White"
FontSize="16"
MaximumHeightRequest="150"
Placeholder="Введите сообщение..."
TextColor="Black" />
<ImageButton
Grid.Column="1"
Clicked="OnSendClicked"
HeightRequest="40"
Source="send.png"
VerticalOptions="End"
WidthRequest="40" />
</Grid>
</Border>
</VerticalStackLayout>
</Grid>
</ContentPage>
public event Action ScrollToEndRequested;
vm.ScrollToEndRequested += () => {
var last = MessagesView.ItemsSource?.Cast<object>()?.LastOrDefault();
if (last != null)
MessagesView.ScrollTo(last, position: ScrollToPosition.End, animate: true);
};
using ChatApp.ViewModel;
namespace ChatApp
{
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
var vm = new ChatViewModel();
vm.ScrollToEndRequested += () =>
{
if (MessagesView.ItemsSource is IEnumerable<object> messages)
{
var last = messages.LastOrDefault();
if (last != null)
MessagesView.ScrollTo(last, position: ScrollToPosition.End, animate: true);
}
};
BindingContext = vm;
}
private async void OnSendClicked(object sender, EventArgs e)
{
var text = UserInput.Text;
UserInput.Text = string.Empty;
if (BindingContext is ChatViewModel vm)
await vm.SendMessageAsync(text);
}
private void OnSend(object sender, EventArgs e) => OnSendClicked(sender, e);
}
}
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ChatApp.Converters
{
public class BoolToColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
=> (bool)value ? (Color)Application.Current.Resources["UserBuble"] : (Color)Application.Current.Resources["BotBuble"];
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
}
using System.Globalization;
using Microsoft.Maui.Controls;
namespace ChatApp.Converters
{
public class BoolToColumnConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
bool isUser = (bool)value;
string target = (parameter as string)?.ToLower();
// "message" означает блок с сообщением (Border), "time" — Label со временем
return target switch
{
"message" => isUser ? 1 : 0,
"time" => isUser ? 0 : 1,
_ => 0
};
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
}
using System.Globalization;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
namespace ChatApp.Converters
{
public class BoolToCornerRadiusConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
bool isUser = (bool)value;
// CornerRadius: TopLeft, TopRight, BottomRight, BottomLeft
if (isUser)
return new CornerRadius(20, 20, 20, 0); // правая сторона (пользователь)
else
return new CornerRadius(20, 20, 0, 20); // левая сторона (бот)
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
}
using System.Globalization;
namespace ChatApp.Converters
{
public class InverseBoolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
=> !(bool)value;
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
}
using System.Globalization;
namespace ChatApp.Converters
{
public class MessageAlignmentConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
bool isUser = (bool)value;
return isUser ? LayoutOptions.End : LayoutOptions.Start;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
}
<?xml version = "1.0" encoding = "UTF-8" ?>
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:ChatApp"
xmlns:converters="clr-namespace:ChatApp.Converters"
xmlns:behaviors="clr-namespace:ChatApp.Behaviors"
x:Class="ChatApp.App">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
<Style TargetType="Entry">
<Style.Behaviors>
<behaviors:RemoveUnderlineBehavior />
</Style.Behaviors>
</Style>
<Style TargetType="Editor">
<Style.Behaviors>
<behaviors:RemoveUnderlineBehavior />
</Style.Behaviors>
</Style>
<converters:BoolToColorConverter x:Key="BoolToColorConverter" />
<converters:MessageAlignmentConverter x:Key="MessageAlignmentConverter" />
<converters:BoolToCornerRadiusConverter x:Key="CornerConverter" />
<converters:BoolToColumnConverter x:Key="ColumnConverter" />
<converters:InverseBoolConverter x:Key="InverseBoolConverter" />
</ResourceDictionary>
</Application.Resources>
</Application>
<Color x:Key="UserBuble">#B9D9E6</Color>
<Color x:Key="BotBuble">#DDDDDD</Color>