Skip to content

Commit 74a6940

Browse files
mattleibowCopilot
andauthored
[AI] Sample: Detail page, semantic search, streaming, and UI polish (#34576)
> [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ## Summary Major improvements to the Essentials AI sample app: new Landmark Detail page with AI features, semantic search, streaming response handler improvements, and comprehensive UI polish across all pages for a modern, edge-to-edge experience on iOS and MacCatalyst. ### New Features - **Landmark Detail Page** — New intermediate page between browsing and trip planning with AI-generated travel tips, similar destinations via semantic search, animated hashtag tags, language picker, full-bleed hero image with scrolling gradient overlay, and Plan Trip navigation - **Semantic Search** — Search bar on Landmarks page filters continent groups using semantic similarity with `Timer`-based debounce (300ms) and tracks recent searches for contextual AI descriptions - **ISemanticSearchService abstraction** — Clean interface backed by `EmbeddingSearchService` (Apple NL embeddings + cosine similarity + hybrid keyword boost + sentence chunking) - **StreamingResponseHandler passthrough mode** — Supports streaming without buffering, with new device tests (`DeliversMultipleIncrementalUpdates`, `ConcatenatedText`) - **PromptBasedSchemaClient** — Prompt-based JSON schema middleware for Phi Silica compatibility ### UI Polish - **Edge-to-edge layout** — All pages use `SafeAreaEdges="None"` on root Grid with back buttons wrapped in `SafeAreaEdges="Container"` - **Scrolling gradient pattern** — Fixed gradient overlay + scrolling gradient that transitions to solid background - **Custom search entry** — Replaced `SearchBar` with borderless `Entry` in rounded `Border` with native border/focus ring removed on iOS/MacCatalyst - **Edge-to-edge horizontal scrollers** — Scroll to screen edges, align with page content padding at rest - **Removed broken BoxView global style** — Was breaking gradient overlays - **Added missing Gray700 color** — Prevented silent navigation crash - **Background → Background property migration** — Replaced `BackgroundColor` with `Background` across all pages ### Code Quality - **IDispatcher injection** instead of `MainThread.BeginInvokeOnMainThread` - **Only-once AI initialization** — Guard against re-entry in `LandmarkDetailViewModel` - **Null safety** — Fixed CS8602 warnings in DataService search methods - **Removed debug logging** ### Navigation Flow `LandmarksPage` (browse + search) → `LandmarkDetailPage` (details + AI tips) → `TripPlanningPage` (itinerary generation) ### Deleted Files - `LandmarkDescriptionView`, `LandmarkTripView` — Replaced by new pages - `LanguagePreferenceService` — Refactored into inline language array ### New Test Coverage - `StreamingResponseHandlerTests/Passthrough.cs` — Unit tests for passthrough streaming mode - `ChatClientStreamingTestsBase` — Updated device tests for streaming scenarios --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 148b9aa commit 74a6940

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+9356
-708
lines changed

src/AI/samples/Essentials.AI.Sample/AI/ItineraryWorkflowExtensions.cs

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,23 +43,19 @@ public static IHostApplicationBuilder AddItineraryWorkflow(this IHostApplication
4343
builder.AddAIAgent(
4444
name: "travel-planner-agent",
4545
instructions: """
46-
You are a simple text parser.
47-
48-
Extract ONLY these 3 values from the user's request:
49-
1. destinationName: The place/location name mentioned (extract it exactly as written)
50-
2. dayCount: The number of days mentioned (default: 3 if not specified)
51-
3. language: The language mentioned for the output (default: English if not specified)
52-
46+
You are a simple text parser. Extract 3 values from the user request.
47+
5348
Rules:
54-
1. ALWAYS extract the raw values.
55-
2. NEVER make up values or interpret the user's intent.
56-
49+
- place: The destination name, exactly as written.
50+
- days: Number of days (default: 3).
51+
- language: Output language (default: English).
52+
5753
Examples:
58-
- "5-day trip to Maui in French" → destinationName: "Maui", dayCount: 5, language: "French"
59-
- "Visit the Great Wall" → destinationName: "Great Wall", dayCount: 3, language: "English"
60-
- "Itinerary for Tokyo" → destinationName: "Tokyo", dayCount: 3, language: "English"
61-
- "Give me a Maui itinerary" → destinationName: "Maui", dayCount: 3, language: "English"
62-
- "Plan a 7 day Japan trip in Spanish" → destinationName: "Japan", dayCount: 7, language: "Spanish"
54+
- "5-day trip to Maui in French" produces {"place": "Maui", "days": 5, "language": "French"}
55+
- "Visit the Great Wall" produces {"place": "Great Wall", "days": 3, "language": "English"}
56+
- "Plan a 7 day Japan trip in Spanish" produces {"place": "Japan", "days": 7, "language": "Spanish"}
57+
- "Give me a Cape Town itinerary" produces {"place": "Cape Town", "days": 3, "language": "English"}
58+
- "Itinerary for Tokyo" produces {"place": "Tokyo", "days": 3, "language": "English"}
6359
""",
6460
chatClientServiceKey: "local-model");
6561

@@ -108,6 +104,7 @@ Return the exact name of the best matching destination from the candidates.
108104
Name = name,
109105
ChatOptions = new ChatOptions
110106
{
107+
Temperature = 0.6f,
111108
Instructions = $"""
112109
You create detailed travel itineraries.
113110

src/AI/samples/Essentials.AI.Sample/AI/WorkflowModels.cs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,31 @@
11
using System.ComponentModel;
2+
using System.Text.Json.Serialization;
23

34
namespace Maui.Controls.Sample.AI;
45

56
/// <summary>
67
/// Result from the Travel Planner Agent - raw extraction of user intent.
7-
/// These values should be extracted exactly as the user stated them, with no interpretation or expansion.
8+
/// Short JSON names (place/days/language) reduce misspelling by small language models.
89
/// </summary>
910
public record TravelPlanResult(
10-
[property: DisplayName("destinationName")]
11-
[property: Description("The exact place/location name as written in the user's request. Extract the raw text only - do NOT interpret, expand, or look up actual landmarks. Example: 'Maui' not 'Maui, Hawaii' or 'Haleakala National Park'.")]
11+
[property: JsonPropertyName("place")]
12+
[property: Description("The destination name mentioned by the user.")]
1213
string DestinationName,
13-
[property: DisplayName("dayCount")]
14-
[property: Description("The exact number of days mentioned by the user. Use 3 as default only if no number is specified.")]
14+
[property: JsonPropertyName("days")]
15+
[property: Description("Number of days for the trip. Default is 3.")]
1516
int DayCount,
16-
[property: DisplayName("language")]
17-
[property: Description("The exact output language mentioned by the user. Use 'English' as default only if no language is specified.")]
17+
[property: JsonPropertyName("language")]
18+
[property: Description("Output language for the itinerary. Default is English.")]
1819
string Language);
1920

2021
/// <summary>
2122
/// Result from the Researcher Agent - the best matching destination (for JSON schema).
2223
/// </summary>
2324
internal record DestinationMatchResult(
24-
[property: DisplayName("matchedDestinationName")]
25+
[property: JsonPropertyName("dest")]
2526
[property: Description("The exact name of the best matching destination from the available list.")]
2627
string MatchedDestinationName,
27-
[property: DisplayName("matchedDestinationDescription")]
28+
[property: JsonPropertyName("desc")]
2829
[property: Description("A brief description of the matched destination, based on the information provided in the additional context.")]
2930
string MatchedDestinationDescription);
3031

src/AI/samples/Essentials.AI.Sample/App.xaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<converters:IsNotNullConverter x:Key="IsNotNullConverter" />
1616
<converters:InvertedBoolConverter x:Key="InvertedBoolConverter" />
1717
<converters:IsNotNullOrEmptyConverter x:Key="IsNotNullOrEmptyConverter" />
18+
<converters:IsPositiveConverter x:Key="IsPositiveConverter" />
1819
</ResourceDictionary>
1920
</Application.Resources>
2021
</Application>

src/AI/samples/Essentials.AI.Sample/AppShell.xaml.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ public AppShell()
88
{
99
InitializeComponent();
1010

11-
// Register routes for navigation
12-
// Only TripPlanningPage is navigable - LandmarkTripView is a child component
11+
Routing.RegisterRoute(nameof(LandmarkDetailPage), typeof(LandmarkDetailPage));
1312
Routing.RegisterRoute(nameof(TripPlanningPage), typeof(TripPlanningPage));
1413
}
1514
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System.Globalization;
2+
3+
namespace Maui.Controls.Sample.Converters;
4+
5+
public class IsPositiveConverter : IValueConverter
6+
{
7+
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
8+
{
9+
return value is int i && i > 0;
10+
}
11+
12+
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
13+
{
14+
throw new NotImplementedException();
15+
}
16+
}

0 commit comments

Comments
 (0)