Skip to content
Draft
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -360,3 +360,5 @@ Resource.designer.cs
*.claude
10.0/AI/ChatClientWithMobile/src/ChatMobile.Api/appsettings.json
10.0/AI/ChatClientWithMobile/src/ChatMobile.Api/appsettings.Development.json
10.0/AI/ChatClientWithVoice/src/ChatVoice.Api/appsettings.Development.json
10.0/AI/ChatClientWithVoice/src/ChatVoice.Api/appsettings.json
30 changes: 30 additions & 0 deletions 10.0/AI/ChatClientWithVoice/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# ChatClientWithVoice

A minimal .NET MAUI sample that extends the ChatClient pattern with two voice-to-text modes using .NET MAUI Community Toolkit:

- Online: `SpeechToText.Default`
- Offline: `OfflineSpeechToText.Default`

It keeps the same credential-distribution API as the mobile sample and focuses on clarity over abstractions.

## Prerequisites

- .NET 10 SDK + MAUI workload
- Start the API project: `ChatVoice.Api`
- Update `appsettings.Development.json` with your Azure OpenAI endpoint/key and Weather API key (for parity with other samples)

## Platforms permissions

- Android: `RECORD_AUDIO` in `Platforms/Android/AndroidManifest.xml`
- iOS/macOS: `NSSpeechRecognitionUsageDescription` and `NSMicrophoneUsageDescription` in `Platforms/iOS/Info.plist`
- Windows: `microphone` capability in `Platforms/Windows/Package.appxmanifest`

## Run

1. Start `ChatVoice.Api` ([http://127.0.0.1:5132/](http://127.0.0.1:5132/) by default in this sample's client)
2. Deploy `ChatVoice.Client` to your device/emulator
3. Tap “Online” or “Offline” to capture speech, then the result auto-sends

## Notes

- This sample omits tool examples for brevity. Add AIFunction tools to `ChatService` if desired, mirroring other samples.
48 changes: 48 additions & 0 deletions 10.0/AI/ChatClientWithVoice/src/ChatClientWithVoice.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatVoice.Api", "ChatVoice.Api\ChatVoice.Api.csproj", "{F1A4D6F5-4C6A-4D31-9D0C-5E7C3B044C6C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatVoice.Client", "ChatVoice.Client\ChatVoice.Client.csproj", "{7E2B1F3E-2A1C-4C08-BB22-2A3D3A2C9B75}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{F1A4D6F5-4C6A-4D31-9D0C-5E7C3B044C6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F1A4D6F5-4C6A-4D31-9D0C-5E7C3B044C6C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F1A4D6F5-4C6A-4D31-9D0C-5E7C3B044C6C}.Debug|x64.ActiveCfg = Debug|Any CPU
{F1A4D6F5-4C6A-4D31-9D0C-5E7C3B044C6C}.Debug|x64.Build.0 = Debug|Any CPU
{F1A4D6F5-4C6A-4D31-9D0C-5E7C3B044C6C}.Debug|x86.ActiveCfg = Debug|Any CPU
{F1A4D6F5-4C6A-4D31-9D0C-5E7C3B044C6C}.Debug|x86.Build.0 = Debug|Any CPU
{F1A4D6F5-4C6A-4D31-9D0C-5E7C3B044C6C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F1A4D6F5-4C6A-4D31-9D0C-5E7C3B044C6C}.Release|Any CPU.Build.0 = Release|Any CPU
{F1A4D6F5-4C6A-4D31-9D0C-5E7C3B044C6C}.Release|x64.ActiveCfg = Release|Any CPU
{F1A4D6F5-4C6A-4D31-9D0C-5E7C3B044C6C}.Release|x64.Build.0 = Release|Any CPU
{F1A4D6F5-4C6A-4D31-9D0C-5E7C3B044C6C}.Release|x86.ActiveCfg = Release|Any CPU
{F1A4D6F5-4C6A-4D31-9D0C-5E7C3B044C6C}.Release|x86.Build.0 = Release|Any CPU
{7E2B1F3E-2A1C-4C08-BB22-2A3D3A2C9B75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7E2B1F3E-2A1C-4C08-BB22-2A3D3A2C9B75}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7E2B1F3E-2A1C-4C08-BB22-2A3D3A2C9B75}.Debug|x64.ActiveCfg = Debug|Any CPU
{7E2B1F3E-2A1C-4C08-BB22-2A3D3A2C9B75}.Debug|x64.Build.0 = Debug|Any CPU
{7E2B1F3E-2A1C-4C08-BB22-2A3D3A2C9B75}.Debug|x86.ActiveCfg = Debug|Any CPU
{7E2B1F3E-2A1C-4C08-BB22-2A3D3A2C9B75}.Debug|x86.Build.0 = Debug|Any CPU
{7E2B1F3E-2A1C-4C08-BB22-2A3D3A2C9B75}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7E2B1F3E-2A1C-4C08-BB22-2A3D3A2C9B75}.Release|Any CPU.Build.0 = Release|Any CPU
{7E2B1F3E-2A1C-4C08-BB22-2A3D3A2C9B75}.Release|x64.ActiveCfg = Release|Any CPU
{7E2B1F3E-2A1C-4C08-BB22-2A3D3A2C9B75}.Release|x64.Build.0 = Release|Any CPU
{7E2B1F3E-2A1C-4C08-BB22-2A3D3A2C9B75}.Release|x86.ActiveCfg = Release|Any CPU
{7E2B1F3E-2A1C-4C08-BB22-2A3D3A2C9B75}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
13 changes: 13 additions & 0 deletions 10.0/AI/ChatClientWithVoice/src/ChatVoice.Api/ChatVoice.Api.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0-preview.7.25380.108" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace ChatVoice.Api.Models;

public class CredentialResponse
{
public AzureOpenAICredentials AzureOpenAI { get; set; } = new();
public string WeatherApiKey { get; set; } = string.Empty;
}

public class AzureOpenAICredentials
{
public string Endpoint { get; set; } = string.Empty;
public string ApiKey { get; set; } = string.Empty;
public string Model { get; set; } = "gpt-4o-mini";
}
46 changes: 46 additions & 0 deletions 10.0/AI/ChatClientWithVoice/src/ChatVoice.Api/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using ChatVoice.Api.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApi();

// Register credential service
builder.Services.AddScoped<ICredentialService, CredentialService>();

// Add CORS for local development
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.UseCors();
}

app.UseHttpsRedirection();

// Credential distribution endpoint
app.MapGet("/api/credentials", (ICredentialService credentialService) =>
{
try
{
return Results.Ok(credentialService.GetCredentials());
}
catch (InvalidOperationException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
})
.WithName("GetCredentials");

app.Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5132",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using ChatVoice.Api.Models;

namespace ChatVoice.Api.Services;

public class CredentialService : ICredentialService
{
private readonly IConfiguration _configuration;

public CredentialService(IConfiguration configuration)
{
_configuration = configuration;
}

public CredentialResponse GetCredentials()
{
var endpoint = _configuration["AzureOpenAI:Endpoint"];
var apiKey = _configuration["AzureOpenAI:ApiKey"];
var model = _configuration["AzureOpenAI:Model"];
var weatherApiKey = _configuration["WeatherApiKey"];

if (string.IsNullOrWhiteSpace(endpoint) || string.IsNullOrWhiteSpace(apiKey) || string.IsNullOrWhiteSpace(weatherApiKey))
{
throw new InvalidOperationException("Missing required configuration values");
}

return new CredentialResponse
{
AzureOpenAI = new AzureOpenAICredentials
{
Endpoint = endpoint!,
ApiKey = apiKey!,
Model = string.IsNullOrWhiteSpace(model) ? "gpt-4o-mini" : model!
},
WeatherApiKey = weatherApiKey!
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using ChatVoice.Api.Models;

namespace ChatVoice.Api.Services;

public interface ICredentialService
{
CredentialResponse GetCredentials();
}
21 changes: 21 additions & 0 deletions 10.0/AI/ChatClientWithVoice/src/ChatVoice.Client/App.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Application x:Class="ChatVoice.Client.App"
xmlns="http://schemas.microsoft.com/dotnet/maui/global"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:fonts="clr-namespace:Fonts;assembly=ChatVoice.Client"
xmlns:converters="clr-namespace:ChatVoice.Client.Converters">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles/Colors.xaml"/>
<ResourceDictionary Source="Resources/Styles/Styles.xaml"/>
</ResourceDictionary.MergedDictionaries>

<!-- Converters -->
<converters:BoolToStatusIconConverter x:Key="BoolToStatusIconConverter"/>
<converters:BoolToCredentialStatusConverter x:Key="BoolToCredentialStatusConverter"/>
<converters:StringToBoolConverter x:Key="StringToBoolConverter"/>
<converters:InvertedBoolConverter x:Key="InvertedBoolConverter"/>
</ResourceDictionary>
</Application.Resources>
</Application>
17 changes: 17 additions & 0 deletions 10.0/AI/ChatClientWithVoice/src/ChatVoice.Client/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace ChatVoice.Client;

public partial class App : Application
{
public App()
{
InitializeComponent();
}

protected override Window CreateWindow(IActivationState? activationState)
{
return new Window(new AppShell())
{
Title = "ChatClientWithMobile"
};
}
}
18 changes: 18 additions & 0 deletions 10.0/AI/ChatClientWithVoice/src/ChatVoice.Client/AppShell.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Shell x:Class="ChatVoice.Client.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/maui/global"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:views="http://schemas.microsoft.com/dotnet/maui/global"
Title="ChatClientWithVoice">

<TabBar>
<ShellContent Title="Chat"
ContentTemplate="{DataTemplate views:MainPage}"
Route="MainPage"/>

<ShellContent Title="Setup"
ContentTemplate="{DataTemplate views:SetupPage}"
Route="SetupPage"/>
</TabBar>

</Shell>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace ChatVoice.Client;

public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net10.0-android;net10.0-ios;net10.0-maccatalyst</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net10.0-windows10.0.19041.0</TargetFrameworks>
<OutputType>Exe</OutputType>
<RootNamespace>ChatVoice.Client</RootNamespace>
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<DefineConstants>$(DefineConstants);MauiAllowImplicitXmlnsDeclaration</DefineConstants>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
<ApplicationTitle>ChatClientWithVoice</ApplicationTitle>
<ApplicationId>com.companyname.chatclientwithvoice</ApplicationId>
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<WindowsPackageType>None</WindowsPackageType>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">15.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">15.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
</PropertyGroup>

<ItemGroup>
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128" />
<MauiImage Include="Resources\Images\*" />
<MauiImage Update="Resources\Images\dotnet_bot.png" Resize="True" BaseSize="300,185" />
<MauiFont Include="Resources\Fonts\*.ttf" />
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
<EmbeddedResource Include="appsettings.json" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.0-preview.7.25380.108" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0-preview.6.25327.7" />
<PackageReference Include="Microsoft.Extensions.AI" Version="9.7.1" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.7.1-preview.1.25365.4" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.1.0" />
<PackageReference Include="CommunityToolkit.Maui" Version="12.1.0" />
<PackageReference Include="CommunityToolkit.Maui.Core" Version="12.1.0" />
<PackageReference Include="Syncfusion.Maui.Toolkit" Version="1.0.6" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Globalization;
using Microsoft.Maui.Controls;

namespace ChatVoice.Client.Converters;

public class BoolToCredentialStatusConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
var has = value is bool b && b;
return has ? "Credentials loaded" : "Credentials not configured";
}

public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => throw new NotImplementedException();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Globalization;
using Microsoft.Maui.Controls;

namespace ChatVoice.Client.Converters;

public class BoolToStatusIconConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
var has = value is bool b && b;
return has ? "✅" : "⚠️";
}

public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => throw new NotImplementedException();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Globalization;
using Microsoft.Maui.Controls;

namespace ChatVoice.Client.Converters;

public class InvertedBoolConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
=> value is bool b ? !b : value;

public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
=> value is bool b ? !b : value;
}
Loading
Loading