diff --git a/Samples/AppContentSearch/README.md b/Samples/AppContentSearch/README.md new file mode 100644 index 000000000..ba7edfef1 --- /dev/null +++ b/Samples/AppContentSearch/README.md @@ -0,0 +1,53 @@ +--- +page_type: sample +languages: +- csharp +products: +- windows +- windows-app-sdk +name: "AppContentSearch Sample" +urlFragment: AppContentSearchSample +description: "Demonstrates how to use the AppContentSearch APIs in Windows App SDK to index and semantically search text content and images in a WinUI3 notes application." +extendedZipContent: +- path: LICENSE + target: LICENSE +--- + + +# AppContentSearch Sample Application + +This sample demonstrates how to use App Content Search's **AppContentIndex APIs** in a **WinUI3** notes application. It shows how to create, manage, and semantically search through the index that includes both text content and images. It also shows how to use use the search results to enable retrieval augmented genaration (RAG) scenarios with language models. + +> **Note**: This sample is targeted and tested for **Windows App SDK 2.0 Experimental2** and **Visual Studio 2022**. The AppContentSearch APIs are experimental and available in Windows App SDK 2.0 experimental2. + + +## Features + +This sample demonstrates: + +- **Creating Index**: Create an index with optional settings. +- **Indexing Content**: Add, update, and remove content from the index +- **Text Content Search**: Query the index for text-based results. +- **Image Content Search**: Query the index for image-based results. +- **Search Results Display**: Display both text and image search results with relevance highlighting and bounding boxes for image matches +- **Retrieval Augmented Generation (RAG)**: Use query search results with language models for retrieval augmented generation (RAG) scenarios. + + +## Prerequisites + +* See [System requirements for Windows app development](https://docs.microsoft.com/windows/apps/windows-app-sdk/system-requirements). +* Make sure that your development environment is set up correctly—see [Install tools for developing apps for Windows 10 and Windows 11](https://docs.microsoft.com/windows/apps/windows-app-sdk/set-up-your-development-environment). +* This sample requires Visual Studio 2022 and .NET 9. + + +## Building and Running the Sample + +* Open the solution file (`AppContentSearch.sln`) in Visual Studio. +* Press Ctrl+Shift+B, or select **Build** \> **Build Solution**. +* Run the application to see the Notes app with integrated search functionality. + + +## Related Documentation and Code Samples + +* [Windows App SDK](https://docs.microsoft.com/windows/apps/windows-app-sdk/) +* [AppContentSearch API Documentation](https://learn.microsoft.com/en-us/windows/ai/apis/app-content-search) \ No newline at end of file diff --git a/Samples/AppContentSearch/cs-winui/AI/IChatClient/PhiSilicaClient.cs b/Samples/AppContentSearch/cs-winui/AI/IChatClient/PhiSilicaClient.cs new file mode 100644 index 000000000..580203ab6 --- /dev/null +++ b/Samples/AppContentSearch/cs-winui/AI/IChatClient/PhiSilicaClient.cs @@ -0,0 +1,242 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +using Microsoft.Extensions.AI; +using Microsoft.Windows.AI.ContentSafety; +using Microsoft.Windows.AI.Text; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Windows.Foundation; + +namespace Notes.AI; + +internal class PhiSilicaClient : IChatClient +{ + // Search Options + private const int DefaultTopK = 50; + private const float DefaultTopP = 0.9f; + private const float DefaultTemperature = 1; + + private LanguageModel? _languageModel; + + public ChatClientMetadata Metadata { get; } + + private PhiSilicaClient() + { + Metadata = new ChatClientMetadata("PhiSilica", new Uri($"file:///PhiSilica")); + } + + private static ChatOptions GetDefaultChatOptions() + { + return new ChatOptions + { + Temperature = DefaultTemperature, + TopP = DefaultTopP, + TopK = DefaultTopK, + }; + } + + public static async Task CreateAsync(CancellationToken cancellationToken = default) + { +#pragma warning disable CA2000 // Dispose objects before losing scope + var phiSilicaClient = new PhiSilicaClient(); +#pragma warning restore CA2000 // Dispose objects before losing scope + + try + { + await phiSilicaClient.InitializeAsync(cancellationToken); + } + catch + { + return null; + } + + return phiSilicaClient; + } + + public Task GetResponseAsync(IList chatMessages, ChatOptions? options = null, CancellationToken cancellationToken = default) => + GetStreamingResponseAsync(chatMessages, options, cancellationToken).ToChatResponseAsync(cancellationToken: cancellationToken); + + public async IAsyncEnumerable GetStreamingResponseAsync(IList chatMessages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + if (_languageModel == null) + { + throw new InvalidOperationException("Language model is not loaded."); + } + + string prompt = GetPromptAsString(chatMessages); + + await foreach (var part in GenerateStreamResponseAsync(prompt, options, cancellationToken)) + { + yield return new ChatResponseUpdate + { + Role = ChatRole.Assistant, + Text = part, + }; + } + } + + private (LanguageModelOptions? ModelOptions, ContentFilterOptions? FilterOptions) GetModelOptions(ChatOptions options) + { + if (options == null) + { + return (null, null); + } + + var languageModelOptions = new LanguageModelOptions + { + Temperature = options.Temperature ?? DefaultTemperature, + TopK = (uint)(options.TopK ?? DefaultTopK), + TopP = (uint)(options.TopP ?? DefaultTopP), + }; + + var contentFilterOptions = new ContentFilterOptions(); + + if (options?.AdditionalProperties?.TryGetValue("input_moderation", out SeverityLevel inputModeration) == true && inputModeration != SeverityLevel.Low) + { + contentFilterOptions.PromptMaxAllowedSeverityLevel = new TextContentFilterSeverity + { + Hate = inputModeration, + Sexual = inputModeration, + Violent = inputModeration, + SelfHarm = inputModeration + }; + } + + if (options?.AdditionalProperties?.TryGetValue("output_moderation", out SeverityLevel outputModeration) == true && outputModeration != SeverityLevel.Low) + { + contentFilterOptions.ResponseMaxAllowedSeverityLevel = new TextContentFilterSeverity + { + Hate = outputModeration, + Sexual = outputModeration, + Violent = outputModeration, + SelfHarm = outputModeration + }; + } + + return (languageModelOptions, contentFilterOptions); + } + + private string GetPromptAsString(IEnumerable chatHistory) + { + if (!chatHistory.Any()) + { + return string.Empty; + } + + StringBuilder prompt = new StringBuilder(); + + for (var i = 0; i < chatHistory.Count(); i++) + { + var message = chatHistory.ElementAt(i); + + if (!string.IsNullOrEmpty(message.Text)) + { + prompt.AppendLine(message.Text); + } + } + + return prompt.ToString(); + } + + public void Dispose() + { + _languageModel?.Dispose(); + _languageModel = null; + } + + public object? GetService(Type serviceType, object? serviceKey = null) + { + return + serviceKey is not null ? null : + _languageModel is not null && serviceType?.IsInstanceOfType(_languageModel) is true ? _languageModel : + serviceType?.IsInstanceOfType(this) is true ? this : + serviceType?.IsInstanceOfType(typeof(ChatOptions)) is true ? GetDefaultChatOptions() : + null; + } + + public static bool IsAvailable() + { + try + { + return LanguageModel.GetReadyState() == Microsoft.Windows.AI.AIFeatureReadyState.Ready; + } + catch + { + return false; + } + } + + private async Task InitializeAsync(CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (!IsAvailable()) + { + await LanguageModel.EnsureReadyAsync(); + } + + cancellationToken.ThrowIfCancellationRequested(); + + _languageModel = await LanguageModel.CreateAsync(); + } + +#pragma warning disable IDE0060 // Remove unused parameter + public async IAsyncEnumerable GenerateStreamResponseAsync(string prompt, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) +#pragma warning restore IDE0060 // Remove unused parameter + { + if (_languageModel == null) + { + throw new InvalidOperationException("Language model is not loaded."); + } + + string currentResponse = string.Empty; + using var newPartEvent = new ManualResetEventSlim(false); + + IAsyncOperationWithProgress? progress; + if (options == null) + { + progress = _languageModel.GenerateResponseAsync(prompt, new LanguageModelOptions()); + } + else + { + var (modelOptions, filterOptions) = GetModelOptions(options); + progress = _languageModel.GenerateResponseAsync(prompt, modelOptions); + } + + progress.Progress = (result, value) => + { + currentResponse = value; + newPartEvent.Set(); + if (cancellationToken.IsCancellationRequested) + { + progress.Cancel(); + } + }; + + while (progress.Status != AsyncStatus.Completed) + { + await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); + + if (newPartEvent.Wait(10, cancellationToken)) + { + yield return currentResponse; + newPartEvent.Reset(); + } + } + + var response = await progress; + + yield return response?.Status switch + { + LanguageModelResponseStatus.BlockedByPolicy => "\nBlocked by policy", + LanguageModelResponseStatus.PromptBlockedByContentModeration => "\nPrompt blocked by content moderation", + LanguageModelResponseStatus.ResponseBlockedByContentModeration => "\nResponse blocked by content moderation", + _ => string.Empty, + }; + } +} \ No newline at end of file diff --git a/Samples/AppContentSearch/cs-winui/AI/Provider/AzureOpenAIProvider.cs b/Samples/AppContentSearch/cs-winui/AI/Provider/AzureOpenAIProvider.cs new file mode 100644 index 000000000..311a88c55 --- /dev/null +++ b/Samples/AppContentSearch/cs-winui/AI/Provider/AzureOpenAIProvider.cs @@ -0,0 +1,202 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +using Azure.AI.OpenAI; +using Microsoft.Extensions.AI; +using Notes.ViewModels; +using OpenAI.Chat; +using System; +using System.ClientModel; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Windows.Storage; +using AppChatRole = Notes.ViewModels.ChatRole; +using ExtChatRole = Microsoft.Extensions.AI.ChatRole; + +namespace Notes.AI; + +public class AzureOpenAIProvider : ILanguageModelProvider +{ + private AzureOpenAIClient? _azureClient; + private ChatClient? _azureChatClient; + + private const string SystemPrimaryPrompt = + "You are a helpful assistant. Your output should be ONLY in plain text. There should be NO markdown elements."; + private const string SystemSecondaryPrompt = + "This is the end of the document snippets.\r\nPlease answer the following user question:\r\n"; + + public string Name => "Azure OpenAI"; + + public bool IsAvailable + { + get + { + var localSettings = ApplicationData.Current.LocalSettings; + string? apiKey = localSettings.Values["AzureOpenAIApiKey"] as string; + string? endpoint = localSettings.Values["AzureOpenAIEndpointUri"] as string; + string? deployment = localSettings.Values["AzureOpenAIDeploymentName"] as string; + + return !string.IsNullOrEmpty(apiKey) && + !string.IsNullOrEmpty(endpoint) && + !string.IsNullOrEmpty(deployment); + } + } + + public Task InitializeAsync(CancellationToken cancellationToken = default) + { + try + { + EnsureAzureClient(); + return Task.FromResult(_azureClient != null && _azureChatClient != null); + } + catch + { + return Task.FromResult(false); + } + } + + public async IAsyncEnumerable SendStreamingRequestAsync( + ChatContext context, + SessionEntryViewModel entry, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + EnsureAzureClient(); + + if (_azureClient == null || _azureChatClient == null) + { + yield return Append(entry, "Error: Azure client is uninitialized. Ensure you set API keys."); + yield break; + } + + var chatMessages = BuildAzureChatMessages(context); + + IAsyncEnumerable? stream = null; + string? startError = null; + try + { + stream = _azureChatClient.CompleteChatStreamingAsync(chatMessages); + } + catch (Exception ex) + { + startError = $"Azure start error: {ex.Message}"; + } + + if (startError != null) + { + yield return Append(entry, startError); + yield break; + } + + bool canceled = false; + Exception? streamEx = null; + + await using (var enumerator = stream!.GetAsyncEnumerator(cancellationToken)) + { + while (true) + { + bool hasNext; + try + { + hasNext = await enumerator.MoveNextAsync(); + } + catch (OperationCanceledException) + { + canceled = true; + break; + } + catch (Exception ex) + { + streamEx = ex; + break; + } + + if (!hasNext) break; + + var update = enumerator.Current; + foreach (var part in update.ContentUpdate) + { + if (!string.IsNullOrEmpty(part.Text)) + { + yield return Append(entry, part.Text); + } + } + } + } + + if (canceled) + { + yield return Append(entry, "[Canceled]"); + } + else if (streamEx != null) + { + yield return Append(entry, $"Azure error: {streamEx.Message}"); + } + } + + private void EnsureAzureClient() + { + if (_azureClient != null && _azureChatClient != null) return; + + try + { + var localSettings = ApplicationData.Current.LocalSettings; + string? apiKey = localSettings.Values["AzureOpenAIApiKey"] as string; + string? endpoint = localSettings.Values["AzureOpenAIEndpointUri"] as string; + string? deployment = localSettings.Values["AzureOpenAIDeploymentName"] as string; + + if (string.IsNullOrEmpty(apiKey) || + string.IsNullOrEmpty(endpoint) || + string.IsNullOrEmpty(deployment)) + { + Debug.WriteLine("Azure init - missing settings."); + return; + } + + _azureClient ??= new AzureOpenAIClient(new Uri(endpoint), new ApiKeyCredential(apiKey)); + _azureChatClient ??= _azureClient.GetChatClient(deployment); + } + catch (Exception ex) + { + Debug.WriteLine("CreateAzureClient - " + ex.Message); + } + } + + private ChatResponseUpdate Append(SessionEntryViewModel entry, string text, ChatResponseUpdate? original = null) + { + ChatSessionViewModel._dispatcherQueue?.TryEnqueue(() => entry.Message += text); + return original ?? new ChatResponseUpdate { Role = ExtChatRole.Assistant, Text = text }; + } + + private List BuildAzureChatMessages(ChatContext context) + { + var list = new List { new SystemChatMessage(SystemPrimaryPrompt) }; + + foreach (var sr in context.Data) + { + if (sr.ContentType == ContentType.Image && sr.ContentBytes != null) + { + var mimeType = SearchResult.ContentSubTypeToMimeType(sr.ContentSubType); + var imagePart = ChatMessageContentPart.CreateImagePart(new BinaryData(sr.ContentBytes), mimeType); + list.Add(new UserChatMessage(imagePart)); + } + else if (sr.Content != null) + { + list.Add(new UserChatMessage(sr.Content)); + } + } + + list.Add(new SystemChatMessage(SystemSecondaryPrompt)); + + foreach (var h in context.ChatHistory.Where(h => !string.IsNullOrEmpty(h.Message))) + { + list.Add(h.Participant == AppChatRole.User + ? new UserChatMessage(h.Message) + : new AssistantChatMessage(h.Message)); + } + + return list; + } +} \ No newline at end of file diff --git a/Samples/AppContentSearch/cs-winui/AI/Provider/FoundryAIProvider.cs b/Samples/AppContentSearch/cs-winui/AI/Provider/FoundryAIProvider.cs new file mode 100644 index 000000000..f3a449905 --- /dev/null +++ b/Samples/AppContentSearch/cs-winui/AI/Provider/FoundryAIProvider.cs @@ -0,0 +1,272 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +using Microsoft.AI.Foundry.Local; +using Microsoft.Extensions.AI; +using Notes.ViewModels; +using OpenAI; +using OpenAI.Chat; +using System; +using System.ClientModel; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Windows.Storage; +using AppChatRole = Notes.ViewModels.ChatRole; +using ExtChatRole = Microsoft.Extensions.AI.ChatRole; + +namespace Notes.AI; + +public class FoundryAIProvider : ILanguageModelProvider +{ + private OpenAIClient? _foundryClient; + private ChatClient? _foundryChatClient; + private string? _foundryModelId; + private readonly Task? _foundryInitTask; + private string? _foundryModelAliasRequested; + + private const string FoundryModelKey = "FoundryModelName"; + private const string SystemPrimaryPrompt = + "You are a helpful assistant. Your output should be ONLY in plain text. There should be NO markdown elements."; + private const string SystemSecondaryPrompt = + "This is the end of the document snippets.\r\nPlease answer the following user question:\r\n"; + + public FoundryAIProvider() + { + // Initialize the task when the provider is created + if (IsAvailable) + { + _foundryInitTask = EnsureFoundryClientAsync(CancellationToken.None); + } + } + + public string Name => "Foundry AI Local"; + + public bool IsAvailable + { + get + { + try + { + var settings = ApplicationData.Current.LocalSettings; + string alias = (settings.Values[FoundryModelKey] as string)?.Trim() ?? ""; + return !string.IsNullOrWhiteSpace(alias) || HasCachedModels(); + } + catch + { + return false; + } + } + } + + public async Task InitializeAsync(CancellationToken cancellationToken = default) + { + try + { + await (_foundryInitTask ?? EnsureFoundryClientAsync(cancellationToken)); + return _foundryClient != null && _foundryChatClient != null && !string.IsNullOrEmpty(_foundryModelId); + } + catch + { + return false; + } + } + + public async IAsyncEnumerable SendStreamingRequestAsync( + ChatContext context, + SessionEntryViewModel entry, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + string? initError = null; + try + { + await (_foundryInitTask ?? EnsureFoundryClientAsync(cancellationToken)); + } + catch (Exception ex) + { + initError = $"Foundry init error: {ex.Message}"; + } + + if (initError != null) + { + yield return Append(entry, initError); + yield break; + } + + if (_foundryClient == null || _foundryChatClient == null || string.IsNullOrEmpty(_foundryModelId)) + { + yield return Append(entry, "Foundry client not ready."); + yield break; + } + + var chatMessages = BuildAzureChatMessages(context); + + IEnumerable? stream = null; + string? startError = null; + try + { + stream = _foundryChatClient.CompleteChatStreaming(chatMessages); + } + catch (Exception ex) + { + startError = $"Foundry start error: {ex.Message}"; + } + + if (startError != null) + { + yield return Append(entry, startError); + yield break; + } + + bool canceled = false; + Exception? streamEx = null; + + using (var enumerator = stream!.GetEnumerator()) + { + while (true) + { + bool hasNext; + try + { + hasNext = enumerator.MoveNext(); + } + catch (OperationCanceledException) + { + canceled = true; + break; + } + catch (Exception ex) + { + streamEx = ex; + break; + } + + if (!hasNext) break; + + if (cancellationToken.IsCancellationRequested) + { + canceled = true; + break; + } + + var update = enumerator.Current; + foreach (var part in update.ContentUpdate.Where(part => !string.IsNullOrEmpty(part.Text))) + { + yield return Append(entry, part.Text); + } + + await Task.Yield(); + } + } + + if (canceled) + { + yield return Append(entry, "[Canceled]"); + } + else if (streamEx != null) + { + yield return Append(entry, $"Foundry streaming error: {streamEx.Message}"); + } + } + + private bool HasCachedModels() + { + try + { + var foundryManager = new FoundryLocalManager(); + var cached = foundryManager.ListCachedModelsAsync().GetAwaiter().GetResult(); + return cached.Count > 0; + } + catch + { + return false; + } + } + + private async Task EnsureFoundryClientAsync(CancellationToken ct) + { + if (_foundryClient != null && _foundryChatClient != null) return; + + var settings = ApplicationData.Current.LocalSettings; + string alias = (settings.Values[FoundryModelKey] as string)?.Trim() ?? ""; + if (string.IsNullOrWhiteSpace(alias)) + { + try + { + var foundryManager = new FoundryLocalManager(); + + var cached = await foundryManager.ListCachedModelsAsync(); + if (cached.Count != 0) + { + ModelInfo modelInfo = cached.First(); + alias = modelInfo.Alias; + } + else + { + throw new InvalidOperationException("Foundry model list could not be retrieved."); + } + } + catch (Exception ex) + { + Debug.WriteLine($"Foundry: Cached model discovery failed: {ex.Message}"); + throw; + } + } + _foundryModelAliasRequested = alias; + + try + { + var manager = await FoundryLocalManager.StartModelAsync(aliasOrModelId: alias); + var modelInfo = await manager.GetModelInfoAsync(aliasOrModelId: alias) + ?? throw new InvalidOperationException("Foundry model info could not be retrieved."); + + _foundryModelId = modelInfo.ModelId; + var key = new ApiKeyCredential(manager.ApiKey); + _foundryClient = new OpenAI.OpenAIClient(key, new OpenAIClientOptions { Endpoint = manager.Endpoint }); + _foundryChatClient = _foundryClient.GetChatClient(_foundryModelId); + } + catch (Exception ex) + { + Debug.WriteLine("Foundry init failed: " + ex.Message); + throw; + } + } + + private ChatResponseUpdate Append(SessionEntryViewModel entry, string text, ChatResponseUpdate? original = null) + { + ChatSessionViewModel._dispatcherQueue?.TryEnqueue(() => entry.Message += text); + return original ?? new ChatResponseUpdate { Role = ExtChatRole.Assistant, Text = text }; + } + + private List BuildAzureChatMessages(ChatContext context) + { + var list = new List { new SystemChatMessage(SystemPrimaryPrompt) }; + + foreach (var sr in context.Data) + { + if (sr.ContentType == ContentType.Image && sr.ContentBytes != null) + { + var mimeType = SearchResult.ContentSubTypeToMimeType(sr.ContentSubType); + var imagePart = ChatMessageContentPart.CreateImagePart(new BinaryData(sr.ContentBytes), mimeType); + list.Add(new UserChatMessage(imagePart)); + } + else if (sr.Content != null) + { + list.Add(new UserChatMessage(sr.Content)); + } + } + + list.Add(new SystemChatMessage(SystemSecondaryPrompt)); + + foreach (var h in context.ChatHistory.Where(h => !string.IsNullOrEmpty(h.Message))) + { + list.Add(h.Participant == AppChatRole.User + ? new UserChatMessage(h.Message) + : new AssistantChatMessage(h.Message)); + } + + return list; + } +} \ No newline at end of file diff --git a/Samples/AppContentSearch/cs-winui/AI/Provider/ILanguageModelProvider.cs b/Samples/AppContentSearch/cs-winui/AI/Provider/ILanguageModelProvider.cs new file mode 100644 index 000000000..390e02d12 --- /dev/null +++ b/Samples/AppContentSearch/cs-winui/AI/Provider/ILanguageModelProvider.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +using Microsoft.Extensions.AI; +using Notes.ViewModels; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Notes.AI; + +public interface ILanguageModelProvider +{ + string Name { get; } + bool IsAvailable { get; } + Task InitializeAsync(CancellationToken cancellationToken = default); + IAsyncEnumerable SendStreamingRequestAsync( + ChatContext context, + SessionEntryViewModel entry, + CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/Samples/AppContentSearch/cs-winui/AI/Provider/PhiSilicaProvider.cs b/Samples/AppContentSearch/cs-winui/AI/Provider/PhiSilicaProvider.cs new file mode 100644 index 000000000..4699e322d --- /dev/null +++ b/Samples/AppContentSearch/cs-winui/AI/Provider/PhiSilicaProvider.cs @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +using Microsoft.Extensions.AI; +using Notes.ViewModels; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using AppChatRole = Notes.ViewModels.ChatRole; +using ExtChatRole = Microsoft.Extensions.AI.ChatRole; + +namespace Notes.AI; + +public class PhiSilicaProvider : ILanguageModelProvider +{ + private PhiSilicaClient? _phiSilicaClient; + private readonly Task? _phiInitTask; + + private const string SystemPrimaryPrompt = + "You are a helpful assistant. Your output should be ONLY in plain text. There should be NO markdown elements."; + private const string SystemSecondaryPrompt = + "This is the end of the document snippets.\r\nPlease answer the following user question:\r\n"; + + public PhiSilicaProvider() + { + // Initialize the task when the provider is created + if (IsAvailable) + { + _phiInitTask = PhiSilicaClient.CreateAsync(); + } + } + + public string Name => "Phi Silica"; + + public bool IsAvailable => PhiSilicaClient.IsAvailable(); + + public async Task InitializeAsync(CancellationToken cancellationToken = default) + { + try + { + await EnsurePhiReadyAsync(cancellationToken); + return _phiSilicaClient != null; + } + catch + { + return false; + } + } + + public async IAsyncEnumerable SendStreamingRequestAsync( + ChatContext context, + SessionEntryViewModel entry, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + string? initError = null; + try + { + await EnsurePhiReadyAsync(cancellationToken); + } + catch (Exception ex) + { + initError = $"Phi model init failed: {ex.Message}"; + } + + if (initError != null) + { + yield return Append(entry, initError); + yield break; + } + + var chatMessages = BuildPhiChatMessages(context); + + IAsyncEnumerable? stream = null; + string? startError = null; + try + { + stream = _phiSilicaClient!.GetStreamingResponseAsync(chatMessages, options: null, cancellationToken); + } + catch (Exception ex) + { + startError = $"Generation start failed: {ex.Message}"; + } + + if (startError != null) + { + yield return Append(entry, startError); + yield break; + } + + bool canceled = false; + Exception? streamEx = null; + + await using (var enumerator = stream!.GetAsyncEnumerator(cancellationToken)) + { + while (true) + { + bool hasNext; + try + { + hasNext = await enumerator.MoveNextAsync(); + } + catch (OperationCanceledException) + { + canceled = true; + break; + } + catch (Exception ex) + { + streamEx = ex; + break; + } + + if (!hasNext) break; + + var update = enumerator.Current; + if (!string.IsNullOrEmpty(update.Text)) + { + yield return Append(entry, update.Text, update); + } + } + } + + if (canceled) + { + yield return Append(entry, "[Canceled]"); + } + else if (streamEx != null) + { + yield return Append(entry, $"Phi streaming error: {streamEx.Message}"); + } + } + + private async Task EnsurePhiReadyAsync(CancellationToken ct) + { + if (_phiSilicaClient != null) return; + + _phiSilicaClient = await (_phiInitTask ?? PhiSilicaClient.CreateAsync(ct)); + if (_phiSilicaClient == null) + { + throw new InvalidOperationException("PhiSilica client could not be initialized."); + } + } + + private ChatResponseUpdate Append(SessionEntryViewModel entry, string text, ChatResponseUpdate? original = null) + { + ChatSessionViewModel._dispatcherQueue?.TryEnqueue(() => entry.Message += text); + return original ?? new ChatResponseUpdate { Role = ExtChatRole.Assistant, Text = text }; + } + + private IList BuildPhiChatMessages(ChatContext context) + { + ; + var list = new List + { + new(ExtChatRole.System, SystemPrimaryPrompt) + }; + + foreach (var sr in context.Data) + { + if (sr.ContentType == ContentType.Image && sr.ContentBytes != null) + { + var title = string.IsNullOrWhiteSpace(sr.Title) ? "image" : sr.Title; + list.Add(new(ExtChatRole.User, $"[Image: {title}]")); + } + else if (!string.IsNullOrEmpty(sr.Content)) + { + list.Add(new(ExtChatRole.User, sr.Content)); + } + } + + list.Add(new(ExtChatRole.System, SystemSecondaryPrompt)); + + foreach (var h in context.ChatHistory.Where(h => !string.IsNullOrEmpty(h.Message))) + { + var role = h.Participant == AppChatRole.User ? ExtChatRole.User : ExtChatRole.Assistant; + list.Add(new(role, h.Message)); + } + + return list; + } +} \ No newline at end of file diff --git a/Samples/AppContentSearch/cs-winui/App.xaml b/Samples/AppContentSearch/cs-winui/App.xaml new file mode 100644 index 000000000..37ac86238 --- /dev/null +++ b/Samples/AppContentSearch/cs-winui/App.xaml @@ -0,0 +1,52 @@ + + + + + + + + + + + + #ee9bbf + + + + + + + + + #ee9bbf + + + + + + + + + #48B1E9 + + + + + + diff --git a/Samples/AppContentSearch/cs-winui/App.xaml.cs b/Samples/AppContentSearch/cs-winui/App.xaml.cs new file mode 100644 index 000000000..de94c6354 --- /dev/null +++ b/Samples/AppContentSearch/cs-winui/App.xaml.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +using Microsoft.UI.Xaml; + +namespace Notes +{ + public partial class App : Application + { + public App() + { + //Initialize.AssemblyInitialize(); + this.InitializeComponent(); + } + + protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) + { + m_window = new MainWindow(); + m_window.Activate(); + } + + private Window? m_window; + public Window? Window => m_window; + } +} diff --git a/Samples/AppContentSearch/cs-winui/Assets/ContosoNote.ico b/Samples/AppContentSearch/cs-winui/Assets/ContosoNote.ico new file mode 100644 index 000000000..32bb47efc Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/ContosoNote.ico differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/LargeTile.scale-100.png b/Samples/AppContentSearch/cs-winui/Assets/LargeTile.scale-100.png new file mode 100644 index 000000000..51c689217 Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/LargeTile.scale-100.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/LargeTile.scale-125.png b/Samples/AppContentSearch/cs-winui/Assets/LargeTile.scale-125.png new file mode 100644 index 000000000..f0eb94498 Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/LargeTile.scale-125.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/LargeTile.scale-150.png b/Samples/AppContentSearch/cs-winui/Assets/LargeTile.scale-150.png new file mode 100644 index 000000000..3b54476c8 Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/LargeTile.scale-150.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/LargeTile.scale-200.png b/Samples/AppContentSearch/cs-winui/Assets/LargeTile.scale-200.png new file mode 100644 index 000000000..229cdc18e Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/LargeTile.scale-200.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/LargeTile.scale-400.png b/Samples/AppContentSearch/cs-winui/Assets/LargeTile.scale-400.png new file mode 100644 index 000000000..e8e7aa79a Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/LargeTile.scale-400.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/LockScreenLogo.scale-200.png b/Samples/AppContentSearch/cs-winui/Assets/LockScreenLogo.scale-200.png new file mode 100644 index 000000000..7440f0d4b Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/LockScreenLogo.scale-200.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/SmallTile.scale-100.png b/Samples/AppContentSearch/cs-winui/Assets/SmallTile.scale-100.png new file mode 100644 index 000000000..b82c5a3cb Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/SmallTile.scale-100.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/SmallTile.scale-125.png b/Samples/AppContentSearch/cs-winui/Assets/SmallTile.scale-125.png new file mode 100644 index 000000000..a15f91c52 Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/SmallTile.scale-125.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/SmallTile.scale-150.png b/Samples/AppContentSearch/cs-winui/Assets/SmallTile.scale-150.png new file mode 100644 index 000000000..f69dd3809 Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/SmallTile.scale-150.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/SmallTile.scale-200.png b/Samples/AppContentSearch/cs-winui/Assets/SmallTile.scale-200.png new file mode 100644 index 000000000..19e723761 Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/SmallTile.scale-200.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/SmallTile.scale-400.png b/Samples/AppContentSearch/cs-winui/Assets/SmallTile.scale-400.png new file mode 100644 index 000000000..a2fe366ee Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/SmallTile.scale-400.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/SplashScreen.scale-100.png b/Samples/AppContentSearch/cs-winui/Assets/SplashScreen.scale-100.png new file mode 100644 index 000000000..ad1bc5e6d Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/SplashScreen.scale-100.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/SplashScreen.scale-125.png b/Samples/AppContentSearch/cs-winui/Assets/SplashScreen.scale-125.png new file mode 100644 index 000000000..35dc340d9 Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/SplashScreen.scale-125.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/SplashScreen.scale-150.png b/Samples/AppContentSearch/cs-winui/Assets/SplashScreen.scale-150.png new file mode 100644 index 000000000..010bb5347 Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/SplashScreen.scale-150.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/SplashScreen.scale-200.png b/Samples/AppContentSearch/cs-winui/Assets/SplashScreen.scale-200.png new file mode 100644 index 000000000..375131d4d Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/SplashScreen.scale-200.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/SplashScreen.scale-400.png b/Samples/AppContentSearch/cs-winui/Assets/SplashScreen.scale-400.png new file mode 100644 index 000000000..b81f05eac Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/SplashScreen.scale-400.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/Square150x150Logo.scale-100.png b/Samples/AppContentSearch/cs-winui/Assets/Square150x150Logo.scale-100.png new file mode 100644 index 000000000..ee89fc465 Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/Square150x150Logo.scale-100.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/Square150x150Logo.scale-125.png b/Samples/AppContentSearch/cs-winui/Assets/Square150x150Logo.scale-125.png new file mode 100644 index 000000000..fe8f36178 Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/Square150x150Logo.scale-125.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/Square150x150Logo.scale-150.png b/Samples/AppContentSearch/cs-winui/Assets/Square150x150Logo.scale-150.png new file mode 100644 index 000000000..5d161f8c3 Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/Square150x150Logo.scale-150.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/Square150x150Logo.scale-200.png b/Samples/AppContentSearch/cs-winui/Assets/Square150x150Logo.scale-200.png new file mode 100644 index 000000000..951c33792 Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/Square150x150Logo.scale-200.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/Square150x150Logo.scale-400.png b/Samples/AppContentSearch/cs-winui/Assets/Square150x150Logo.scale-400.png new file mode 100644 index 000000000..46488583b Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/Square150x150Logo.scale-400.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png new file mode 100644 index 000000000..ef7ee491e Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png new file mode 100644 index 000000000..2909fcc9b Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png new file mode 100644 index 000000000..32bb47efc Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png new file mode 100644 index 000000000..ae38ffce0 Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png new file mode 100644 index 000000000..1934af795 Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.altform-unplated_targetsize-16.png b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.altform-unplated_targetsize-16.png new file mode 100644 index 000000000..ef7ee491e Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.altform-unplated_targetsize-16.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.altform-unplated_targetsize-256.png b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.altform-unplated_targetsize-256.png new file mode 100644 index 000000000..32bb47efc Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.altform-unplated_targetsize-256.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.altform-unplated_targetsize-32.png b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.altform-unplated_targetsize-32.png new file mode 100644 index 000000000..ae38ffce0 Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.altform-unplated_targetsize-32.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.altform-unplated_targetsize-48.png b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.altform-unplated_targetsize-48.png new file mode 100644 index 000000000..1934af795 Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.altform-unplated_targetsize-48.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.scale-100.png b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.scale-100.png new file mode 100644 index 000000000..c83a0f4d2 Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.scale-100.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.scale-125.png b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.scale-125.png new file mode 100644 index 000000000..4806aa343 Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.scale-125.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.scale-150.png b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.scale-150.png new file mode 100644 index 000000000..b4bd2c16f Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.scale-150.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.scale-200.png b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.scale-200.png new file mode 100644 index 000000000..2177aef61 Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.scale-200.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.scale-400.png b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.scale-400.png new file mode 100644 index 000000000..27172f013 Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.scale-400.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.targetsize-16.png b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.targetsize-16.png new file mode 100644 index 000000000..ef7ee491e Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.targetsize-16.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.targetsize-24.png b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.targetsize-24.png new file mode 100644 index 000000000..2909fcc9b Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.targetsize-24.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 000000000..2909fcc9b Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.targetsize-256.png b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.targetsize-256.png new file mode 100644 index 000000000..32bb47efc Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.targetsize-256.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.targetsize-32.png b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.targetsize-32.png new file mode 100644 index 000000000..ae38ffce0 Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.targetsize-32.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.targetsize-48.png b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.targetsize-48.png new file mode 100644 index 000000000..1934af795 Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/Square44x44Logo.targetsize-48.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/StoreLogo.backup.png b/Samples/AppContentSearch/cs-winui/Assets/StoreLogo.backup.png new file mode 100644 index 000000000..a4586f26b Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/StoreLogo.backup.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/StoreLogo.scale-100.png b/Samples/AppContentSearch/cs-winui/Assets/StoreLogo.scale-100.png new file mode 100644 index 000000000..fca447437 Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/StoreLogo.scale-100.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/StoreLogo.scale-125.png b/Samples/AppContentSearch/cs-winui/Assets/StoreLogo.scale-125.png new file mode 100644 index 000000000..af245c5fe Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/StoreLogo.scale-125.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/StoreLogo.scale-150.png b/Samples/AppContentSearch/cs-winui/Assets/StoreLogo.scale-150.png new file mode 100644 index 000000000..3bd4a8828 Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/StoreLogo.scale-150.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/StoreLogo.scale-200.png b/Samples/AppContentSearch/cs-winui/Assets/StoreLogo.scale-200.png new file mode 100644 index 000000000..6c738e19c Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/StoreLogo.scale-200.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/StoreLogo.scale-400.png b/Samples/AppContentSearch/cs-winui/Assets/StoreLogo.scale-400.png new file mode 100644 index 000000000..2ae7755db Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/StoreLogo.scale-400.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/Wide310x150Logo.scale-100.png b/Samples/AppContentSearch/cs-winui/Assets/Wide310x150Logo.scale-100.png new file mode 100644 index 000000000..1fca97b62 Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/Wide310x150Logo.scale-100.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/Wide310x150Logo.scale-125.png b/Samples/AppContentSearch/cs-winui/Assets/Wide310x150Logo.scale-125.png new file mode 100644 index 000000000..093e5cfd2 Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/Wide310x150Logo.scale-125.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/Wide310x150Logo.scale-150.png b/Samples/AppContentSearch/cs-winui/Assets/Wide310x150Logo.scale-150.png new file mode 100644 index 000000000..2c3c8d432 Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/Wide310x150Logo.scale-150.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/Wide310x150Logo.scale-200.png b/Samples/AppContentSearch/cs-winui/Assets/Wide310x150Logo.scale-200.png new file mode 100644 index 000000000..ad1bc5e6d Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/Wide310x150Logo.scale-200.png differ diff --git a/Samples/AppContentSearch/cs-winui/Assets/Wide310x150Logo.scale-400.png b/Samples/AppContentSearch/cs-winui/Assets/Wide310x150Logo.scale-400.png new file mode 100644 index 000000000..375131d4d Binary files /dev/null and b/Samples/AppContentSearch/cs-winui/Assets/Wide310x150Logo.scale-400.png differ diff --git a/Samples/AppContentSearch/cs-winui/Controls/AttachmentView.xaml b/Samples/AppContentSearch/cs-winui/Controls/AttachmentView.xaml new file mode 100644 index 000000000..0d0e2cc5b --- /dev/null +++ b/Samples/AppContentSearch/cs-winui/Controls/AttachmentView.xaml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Samples/AppContentSearch/cs-winui/Controls/AttachmentView.xaml.cs b/Samples/AppContentSearch/cs-winui/Controls/AttachmentView.xaml.cs new file mode 100644 index 000000000..4a74d688c --- /dev/null +++ b/Samples/AppContentSearch/cs-winui/Controls/AttachmentView.xaml.cs @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media.Imaging; +using Notes.Models; +using Notes.ViewModels; +using System; +using System.Threading.Tasks; +using Windows.Foundation; +using Windows.Storage; + +namespace Notes.Controls +{ + public sealed partial class AttachmentView : UserControl + { + private readonly DispatcherQueue? _dispatcher; + private Rect? _boundingBox; + + public AttachmentViewModel? AttachmentVM { get; set; } + public bool AutoScrollEnabled { get; set; } = true; + + public AttachmentView() + { + this.InitializeComponent(); + this.Visibility = Visibility.Collapsed; + this._dispatcher = DispatcherQueue.GetForCurrentThread(); + AttachmentImage.ImageOpened += AttachmentImage_ImageOpened; + SizeChanged += AttachmentView_SizeChanged; + } + + public void Show() + { + this.Visibility = Visibility.Visible; + TryDrawBoundingBox(); + } + + public void Hide() + { + AttachmentImage.Source = null; + AttachmentImageTextRect.Visibility = Visibility.Collapsed; + _boundingBox = null; + this.Visibility = Visibility.Collapsed; + } + + private void Root_Tapped(object sender, TappedRoutedEventArgs e) + { + // hide the search view only when the background was tapped but not any of the content inside + if (e.OriginalSource as Grid == Root) + this.Hide(); + } + + public async Task UpdateAttachment(AttachmentViewModel attachment, string? attachmentText = null, Rect? boundingBox = null) + { + AttachmentImageTextRect.Visibility = Visibility.Collapsed; + _boundingBox = boundingBox; + + AttachmentVM = attachment; + StorageFolder attachmentsFolder = await Utils.GetAttachmentsFolderAsync(); + StorageFile attachmentFile = await attachmentsFolder.GetFileAsync(attachment.Attachment.Filename); + + switch (AttachmentVM.Attachment.Type) + { + case NoteAttachmentType.Image: + ImageGrid.Visibility = Visibility.Visible; + AttachmentImage.Source = new BitmapImage(new Uri(attachmentFile.Path)); + break; + case NoteAttachmentType.Video: + case NoteAttachmentType.Audio: + throw new NotSupportedException("audio and video files are not supported"); + } + } + + private void CloseButton_Click(object sender, RoutedEventArgs e) + { + this.Hide(); + } + + private void AttachmentImage_ImageOpened(object sender, RoutedEventArgs e) + { + TryDrawBoundingBox(); + } + + private void AttachmentView_SizeChanged(object sender, SizeChangedEventArgs e) + { + // Recalculate if layout changes (zoom / window size) + TryDrawBoundingBox(); + } + + private void TryDrawBoundingBox() + { + AttachmentImageTextRect.Visibility = Visibility.Collapsed; + + if (_boundingBox is null) + { + return; + } + + // Respect user preference + var settings = Windows.Storage.ApplicationData.Current.LocalSettings; + if (settings.Values.TryGetValue("ShowBoundingBoxes", out var val) && val is bool showBoxes && !showBoxes) + { + return; + } + + if (AttachmentImage.Source is not BitmapImage bmp) + { + return; + } + + double imageW = AttachmentImage.ActualWidth; + double imageH = AttachmentImage.ActualHeight; + if (imageW <= 0 || imageH <= 0) + { + return; + } + + double pixelW = bmp.PixelWidth > 0 ? bmp.PixelWidth : imageW; + double pixelH = bmp.PixelHeight > 0 ? bmp.PixelHeight : imageH; + + var raw = _boundingBox.Value; + + double normX = raw.X / pixelW; + double normY = raw.Y / pixelH; + double normW = raw.Width / pixelW; + double normH = raw.Height / pixelH; + + normX = Math.Clamp(normX, 0, 1); + normY = Math.Clamp(normY, 0, 1); + normW = Math.Clamp(normW, 0, 1 - normX); + normH = Math.Clamp(normH, 0, 1 - normY); + + double rectW = normW * imageW; + double rectH = normH * imageH; + double rectX = normX * imageW; + double rectY = normY * imageH; + + if (rectW <= 0 || rectH <= 0) + return; + + // Set Rectangle properties + AttachmentImageTextRect.Width = rectW; + AttachmentImageTextRect.Height = rectH; + AttachmentImageTextRect.HorizontalAlignment = HorizontalAlignment.Left; + AttachmentImageTextRect.VerticalAlignment = VerticalAlignment.Top; + AttachmentImageTextRect.Margin = new Thickness(rectX, rectY, 0, 0); + AttachmentImageTextRect.Stroke = new Microsoft.UI.Xaml.Media.SolidColorBrush(Windows.UI.Color.FromArgb(255, 255, 80, 0)); + AttachmentImageTextRect.StrokeThickness = 3; + AttachmentImageTextRect.Fill = new Microsoft.UI.Xaml.Media.SolidColorBrush(Windows.UI.Color.FromArgb(40, 255, 80, 0)); + AttachmentImageTextRect.RadiusX = 4; + AttachmentImageTextRect.RadiusY = 4; + AttachmentImageTextRect.Visibility = Visibility.Visible; + } + } +} \ No newline at end of file diff --git a/Samples/AppContentSearch/cs-winui/Controls/ChatSessionView.xaml b/Samples/AppContentSearch/cs-winui/Controls/ChatSessionView.xaml new file mode 100644 index 000000000..2a3a5f295 --- /dev/null +++ b/Samples/AppContentSearch/cs-winui/Controls/ChatSessionView.xaml @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Samples/AppContentSearch/cs-winui/Controls/ChatSessionView.xaml.cs b/Samples/AppContentSearch/cs-winui/Controls/ChatSessionView.xaml.cs new file mode 100644 index 000000000..61a9f691d --- /dev/null +++ b/Samples/AppContentSearch/cs-winui/Controls/ChatSessionView.xaml.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Input; +using Notes.ViewModels; +using System.Linq; +using System.Threading.Tasks; +using Windows.System; + +namespace Notes.Controls; + +public sealed partial class ChatSessionView : UserControl +{ + public ChatSessionViewModel? ChatSessionViewModel { get; private set; } + + public ChatSessionView() + { + this.InitializeComponent(); + + SendButton.Click += SendButton_Click; + RequestTextBox.KeyDown += RequestTextBox_KeyDown; + } + + public void InitializeChatSessionViewModel() + { + if (ChatSessionViewModel == null) + { + ChatSessionViewModel = new ChatSessionViewModel(); + } + } + + private void SendButton_Click(object sender, RoutedEventArgs e) + { + var message = RequestTextBox.Text; + RequestTextBox.Text = string.Empty; + + Task.Run(() => + { + _ = SendRequest(message); + }); + } + + private void RequestTextBox_KeyDown(object sender, KeyRoutedEventArgs e) + { + if (e.Key == VirtualKey.Enter) + { + var message = RequestTextBox.Text; + RequestTextBox.Text = string.Empty; + + Task.Run(() => + { + _ = SendRequest(message); + }); + } + } + + public async Task ClearChatHistory() + { + if (ChatSessionViewModel != null) + { + await ChatSessionViewModel.ClearChatHistory(); + } + } + + private async void ClearChatButton_Click(object sender, RoutedEventArgs e) + { + await ClearChatHistory(); + } + + private async Task SendRequest(string message) + { + if (!string.IsNullOrEmpty(message) && ChatSessionViewModel != null) + { + return await ChatSessionViewModel.SendRequest(message); + } + + return false; + } + + private async void OnReferenceClick(object sender, RoutedEventArgs e) + { + var context = await AppDataContext.GetCurrentAsync(); + var button = sender as Button; + + if (button?.Tag is SearchResult item && MainWindow.Instance != null) + { + if (item.ContentType == ContentType.Note) + { + await MainWindow.Instance.SelectNoteById(item.SourceId); + } + else + { + var attachment = context.Attachments.Where(a => a.Id == item.SourceId).FirstOrDefault(); + + if (attachment != null) + { + var note = context.Notes.Where(n => n.Id == attachment.NoteId).FirstOrDefault(); + + if (note != null) + { + await MainWindow.Instance.SelectNoteById(note.Id, attachment.Id, item.MostRelevantSentence); + } + } + } + } + } +} diff --git a/Samples/AppContentSearch/cs-winui/Controls/SearchView.xaml b/Samples/AppContentSearch/cs-winui/Controls/SearchView.xaml new file mode 100644 index 000000000..567630d56 --- /dev/null +++ b/Samples/AppContentSearch/cs-winui/Controls/SearchView.xaml @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + No Results Found + + + + Image Results + + + + + + + + + + + + Text Results + + + + + + + + + + + + + + + + + + + + diff --git a/Samples/AppContentSearch/cs-winui/Controls/SearchView.xaml.cs b/Samples/AppContentSearch/cs-winui/Controls/SearchView.xaml.cs new file mode 100644 index 000000000..f95b25989 --- /dev/null +++ b/Samples/AppContentSearch/cs-winui/Controls/SearchView.xaml.cs @@ -0,0 +1,276 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +using Microsoft.UI; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Media.Imaging; +using Microsoft.UI.Xaml.Shapes; +using Notes.ViewModels; +using System; +using System.Diagnostics; +using System.Linq; +using Windows.Foundation; +using Windows.Storage; + +namespace Notes.Controls +{ + public sealed partial class SearchView : UserControl + { + public SearchViewModel ViewModel { get; } = new SearchViewModel(); + + public SearchView() + { + this.InitializeComponent(); + } + + private void HideResultsPanel() + { + ViewModel.ShowResults = false; + } + + private async void ResultsItemsView_ItemInvoked(ItemsView sender, ItemsViewItemInvokedEventArgs e) + { + var context = await AppDataContext.GetCurrentAsync(); + var item = e.InvokedItem as SearchResult; + + if (MainWindow.Instance != null && item != null) + { + if (item.ContentType == ContentType.Note) + { + await MainWindow.Instance.SelectNoteById(item.SourceId); + } + else + { + var attachment = context.Attachments.Where(a => a.Id == item.SourceId).FirstOrDefault(); + + if (attachment != null) + { + var note = context.Notes.Where(n => n.Id == attachment.NoteId).FirstOrDefault(); + if (note != null) + { + await MainWindow.Instance.SelectNoteById( + note.Id, + attachment.Id, + item.MostRelevantSentence ?? null, + item.BoundingBox); + } + } + } + } + + HideResultsPanel(); + } + + private void Root_LostFocus(object sender, RoutedEventArgs e) + { + HideResultsPanel(); + } + + public void StartIndexProgressBar() + { + IndexProgressBar.IsIndeterminate = true; + IndexProgressBar.Opacity = 1; + IndexProgressBar.Visibility = Visibility.Visible; + } + + private void SetSearchBoxState(bool isEnabled, Brush foreground, string glyph) + { + SearchBoxQueryIcon.Foreground = foreground; + SearchBoxQueryIcon.Glyph = glyph; + this.IsEnabled = isEnabled; + } + + private void ShowProgressBar(bool isIndeterminate, double opacity, bool isVisible) + { + IndexProgressBar.IsIndeterminate = isIndeterminate; + IndexProgressBar.Opacity = opacity; + IndexProgressBar.Visibility = isVisible ? Visibility.Visible : Visibility.Collapsed; + } + + public void SetSearchBoxDisabled() + { + SetSearchBoxState(false, GetThemeBrush("TextFillColorPrimaryBrush"), "\uE721"); + ShowProgressBar(false, 1, false); + } + + public void SetSearchBoxInitializing() + { + SetSearchBoxState(false, GetThemeBrush("TextFillColorPrimaryBrush"), "\uE721"); + ShowProgressBar(true, 1, true); + } + + public void SetSearchBoxInitializingCompleted() + { + SetSearchBoxState(true, GetThemeBrush("TextFillColorPrimaryBrush"), "\uE721"); + ShowProgressBar(false, 1, false); + } + + public void SetSearchBoxIndexingCompleted() + { + SetSearchBoxState(true, GetThemeBrush("AIAccentGradientBrush"), "\uED37"); + ShowProgressBar(false, 1, false); + } + + public void StartIndexProgressBarStaging() + { + ShowProgressBar(true, 0.5, true); + } + + public void SetIndexProgressBar(double percent) + { + if (IndexProgressBar.Visibility == Visibility.Collapsed) + { + IndexProgressBar.Visibility = Visibility.Visible; + } + IndexProgressBar.Opacity = 1; + IndexProgressBar.IsIndeterminate = false; + IndexProgressBar.Value = percent; + } + + private Brush GetThemeBrush(string key) + { + return Application.Current.Resources[key] as Brush ?? new SolidColorBrush(Colors.Gray); + } + + private void SearchBox_QuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args) + { + ViewModel.HandleQuerySubmitted(sender.Text); + } + + private void ItemContainer_BringIntoViewRequested(UIElement sender, BringIntoViewRequestedEventArgs args) + { + // When the popup is being hidden we get a spurious BringIntoView request which the Repeater doesn't handle well. + // We can avoid that by marking the event as handled when the results panel is not visible. + if (!ViewModel.ShowResults) + { + args.Handled = true; + } + } + + private void ResultImage_ImageOpened(object sender, RoutedEventArgs e) + { + if (sender is Image img && img.Parent is Grid g) + { + UpdateBoundingBoxVisibility(g); + } + } + + private void ImageHost_SizeChanged(object sender, SizeChangedEventArgs e) + { + if (sender is Grid g) + { + UpdateBoundingBoxVisibility(g); + } + } + + private void UpdateBoundingBoxVisibility(Grid host) + { + if (host.Tag is not SearchResult sr || sr.BoundingBox is null) + { + HideBoundingBox(host); + return; + } + + // Read user preference (defaults to true if missing) + bool showBoxes = true; + var settings = ApplicationData.Current.LocalSettings; + if (settings.Values.TryGetValue("ShowBoundingBoxes", out var val) && val is bool b) + { + showBoxes = b; + } + + var overlayRect = host.Children.FirstOrDefault(c => c is Rectangle rect && rect.Name == "OverlayRectangle") as Rectangle; + var img = host.Children.FirstOrDefault(c => c is Image image && image.Name == "ResultImage") as Image; + + if (overlayRect == null || img == null) + return; + + if (showBoxes) + { + Rect bounds = GetBoundingBox(host, img, sr.BoundingBox.Value); + ShowBoundingBox(overlayRect, bounds); + } + else + { + HideBoundingBox(host); + } + } + + private Rect GetBoundingBox(Grid host, Image img, Rect rawBoundingBox) + { + double hostW = host.ActualWidth; + double hostH = host.ActualHeight; + if (hostW <= 0 || hostH <= 0) + return Rect.Empty; + + double pixelW = (img.Source as BitmapImage)?.PixelWidth ?? hostW; + double pixelH = (img.Source as BitmapImage)?.PixelHeight ?? hostH; + if (pixelW <= 0 || pixelH <= 0) + return Rect.Empty; + + // Calculate rendered size with Uniform scaling + double aspect = pixelW / pixelH; + double drawW = hostW; + double drawH = drawW / aspect; + if (drawH > hostH) + { + drawH = hostH; + drawW = drawH * aspect; + } + double offsetX = (hostW - drawW) / 2.0; + double offsetY = (hostH - drawH) / 2.0; + + // Normalize if rectangle looks like pixel coordinates + bool looksPixel = rawBoundingBox.X > 1 || rawBoundingBox.Y > 1 || rawBoundingBox.Width > 1 || rawBoundingBox.Height > 1; + double normX = looksPixel ? rawBoundingBox.X / pixelW : rawBoundingBox.X; + double normY = looksPixel ? rawBoundingBox.Y / pixelH : rawBoundingBox.Y; + double normW = looksPixel ? rawBoundingBox.Width / pixelW : rawBoundingBox.Width; + double normH = looksPixel ? rawBoundingBox.Height / pixelH : rawBoundingBox.Height; + + // Clamp normalized values for robustness + normX = Math.Clamp(normX, 0, 1); + normY = Math.Clamp(normY, 0, 1); + normW = Math.Clamp(normW, 0, 1 - normX); + normH = Math.Clamp(normH, 0, 1 - normY); + + // Convert to screen coordinates + double x = offsetX + normX * drawW; + double y = offsetY + normY * drawH; + double w = normW * drawW; + double h = normH * drawH; + + return new Rect(x, y, w, h); + } + + private void ShowBoundingBox(Rectangle overlayRect, Rect bounds) + { + if (bounds.IsEmpty || bounds.Width <= 0 || bounds.Height <= 0) + { + overlayRect.Visibility = Visibility.Collapsed; + return; + } + + // Position and size the rectangle + overlayRect.Width = bounds.Width; + overlayRect.Height = bounds.Height; + overlayRect.Margin = new Thickness(bounds.X, bounds.Y, 0, 0); + overlayRect.HorizontalAlignment = HorizontalAlignment.Left; + overlayRect.VerticalAlignment = VerticalAlignment.Top; + + // Make it visible + overlayRect.Visibility = Visibility.Visible; + + Debug.WriteLine($"Shown bounding box rectangle (screen coords: {bounds.X:F1},{bounds.Y:F1},{bounds.Width:F1},{bounds.Height:F1})"); + } + + private void HideBoundingBox(Grid host) + { + var overlayRect = host.Children.FirstOrDefault(c => c is Rectangle rect && rect.Name == "OverlayRectangle") as Rectangle; + if (overlayRect != null) + { + overlayRect.Visibility = Visibility.Collapsed; + } + } + } +} \ No newline at end of file diff --git a/Samples/AppContentSearch/cs-winui/MainWindow.xaml b/Samples/AppContentSearch/cs-winui/MainWindow.xaml new file mode 100644 index 000000000..35e6a7bc1 --- /dev/null +++ b/Samples/AppContentSearch/cs-winui/MainWindow.xaml @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Samples/AppContentSearch/cs-winui/MainWindow.xaml.cs b/Samples/AppContentSearch/cs-winui/MainWindow.xaml.cs new file mode 100644 index 000000000..14dce1859 --- /dev/null +++ b/Samples/AppContentSearch/cs-winui/MainWindow.xaml.cs @@ -0,0 +1,267 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.Windows.AI.Search.Experimental.AppContentIndex; +using Notes.Controls; +using Notes.Pages; +using Notes.ViewModels; +using System; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; + +namespace Notes +{ + public sealed partial class MainWindow : Window + { + private static ChatSessionView? _chatSessionView; + private static SearchView? _searchView; + private static MainWindow? _instance; + private static AppContentIndexer? _appContentIndexer; + + public static ChatSessionView? ChatSessionView => _chatSessionView; + public static SearchView? SearchView => _searchView; + public static MainWindow? Instance => _instance; + public static AppContentIndexer? AppContentIndexer => _appContentIndexer; + + public ViewModel VM; + + private readonly Task _initializeAppContentIndexerTask; + + public MainWindow() + { + VM = new ViewModel(); + this.InitializeComponent(); + + this.ExtendsContentIntoTitleBar = true; + this.SetTitleBar(AppTitleBar); + + this.Title = AppTitleBar.Title; + this.AppWindow.SetIcon("Assets/ContosoNote.ico"); + + _instance = this; + _searchView = AppSearchView; + _chatSessionView = AppChatSessionView; + + VM.Notes.CollectionChanged += Notes_CollectionChanged; + + _initializeAppContentIndexerTask = InitializeAppContentIndexerAsync(); + + DispatcherQueue.TryEnqueue(async () => + { + _searchView?.SetSearchBoxInitializing(); + + try + { + await _initializeAppContentIndexerTask; + } + catch (Exception ex) + { + Debug.WriteLine("Indexer init failed: " + ex); + // Inspect ex.HResult, Message, InnerException + } + }); + } + + public async Task SelectNoteById(int id, int? attachmentId = null, string? attachmentText = null, Windows.Foundation.Rect? boundingBox = null) + { + var note = VM.Notes.Where(n => n.Note.Id == id).FirstOrDefault(); + + if (note != null) + { + NavView.SelectedItem = note; + + if (attachmentId.HasValue) + { + var attachmentViewModel = note.Attachments.Where(a => a.Attachment.Id == attachmentId.Value).FirstOrDefault(); + if (attachmentViewModel == null) + { + var context = await AppDataContext.GetCurrentAsync(); + var attachment = context.Attachments.Where(a => a.Id == attachmentId.Value).FirstOrDefault(); + if (attachment == null) + { + return; + } + + attachmentViewModel = new AttachmentViewModel(attachment); + } + + OpenAttachmentView(attachmentViewModel, attachmentText, boundingBox); + } + } + } + + private async Task InitializeAppContentIndexerAsync() + { + GetOrCreateIndexResult? getOrCreateResult = null; + await Task.Run(() => + { + getOrCreateResult = Microsoft.Windows.AI.Search.Experimental.AppContentIndex.AppContentIndexer.GetOrCreateIndex("NotesIndex"); + if (getOrCreateResult == null) + { + throw new Exception("GetOrCreateIndexResult is null"); + } + if (!getOrCreateResult.Succeeded) + { + throw getOrCreateResult.ExtendedError; + } + + _appContentIndexer = getOrCreateResult.Indexer; + }); + + DispatcherQueue.TryEnqueue(() => + { + ChatPaneToggleButton.IsEnabled = true; + AppSearchView.SetSearchBoxInitializingCompleted(); + + var status = getOrCreateResult?.Status; + + switch (status) + { + case GetOrCreateIndexStatus.CreatedNew: + _ = IndexAllAsync(); + break; + case GetOrCreateIndexStatus.OpenedExisting: + AppSearchView.SetSearchBoxIndexingCompleted(); + break; + default: + AppSearchView.SetSearchBoxInitializingCompleted(); + break; + } + }); + } + + private void NavView_Loaded(object sender, RoutedEventArgs e) + { + if (NavView.MenuItems.Count > 0) + NavView.SelectedItem = NavView.MenuItems[0]; + } + + private void Notes_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (NavView.SelectedItem == null && VM.Notes.Count > 0) + NavView.SelectedItem = VM.Notes[0]; + } + + private async void NewButton_Click(object sender, RoutedEventArgs e) + { + var note = await VM.CreateNewNote(); + NavView.SelectedItem = note; + } + + private void NavView_SelectionChanged(NavigationView sender, Microsoft.UI.Xaml.Controls.NavigationViewSelectionChangedEventArgs args) + { + if (args.IsSettingsSelected) + { + NavFrame.Navigate(typeof(Notes.Pages.SettingsPage)); + return; + } + if (args.SelectedItem is NoteViewModel note) + { + NavFrame.Navigate(typeof(NotesPage), note); + } + } + + private async void DeleteMenuItem_Click(object sender, RoutedEventArgs e) + { + var note = (sender as FrameworkElement)?.Tag as NoteViewModel; + + if (VM != null && note != null) + { + await VM.RemoveNoteAsync(note); + } + + if (NavFrame.CurrentSourcePageType == typeof(NotesPage) && NavFrame.CanGoBack) + { + NavFrame.GoBack(); + } + } + + private void ToggleChatPane() + { + AppChatSessionView.Visibility = + AppChatSessionView.Visibility == Visibility.Visible ? + Visibility.Collapsed : + Visibility.Visible; + } + + private void ChatPaneToggleButton_Click(object sender, RoutedEventArgs e) + { + ToggleChatPane(); + + if (_chatSessionView?.ChatSessionViewModel == null) + { + _chatSessionView?.InitializeChatSessionViewModel(); + } + } + + public async void OpenAttachmentView(AttachmentViewModel attachment, string? attachmentText = null, Windows.Foundation.Rect? boundingBox = null) + { + await AttachmentView.UpdateAttachment(attachment, attachmentText, boundingBox); + AttachmentView.Show(); + } + + private async Task IndexAllAsync() + { + _searchView?.SetSearchBoxInitializingCompleted(); + + var notes = this.VM.Notes; + int total = notes.Count; + + var progress = new Progress(percent => + { + _searchView?.SetIndexProgressBar(percent); + Debug.WriteLine($"Indexing progress: {percent:F2}%"); + }); + + // Add all the files to the staging phase of the index. + int index = 0; + foreach (var note in notes) + { + index++; + double percent = (double)index / total * 100; + ((IProgress)progress).Report(percent); + + await note.ReindexAsync(); + } + + // After adding all files to the staging phase of the index, we wait until the index fully completes. + // Results may be partial during the staging phase. + _searchView?.StartIndexProgressBarStaging(); + + if (_appContentIndexer != null) + { + await _appContentIndexer.WaitForIndexingIdleAsync(TimeSpan.MaxValue); + } + + // Indexing is fully completed. + _searchView?.SetSearchBoxIndexingCompleted(); + } + + private async void IndexButton_Click(object sender, RoutedEventArgs e) + { + if (_appContentIndexer != null) + { + await IndexAllAsync(); + } + } + + private async void DeleteIndexButton_Click(object sender, RoutedEventArgs e) + { + await NoteViewModel.ManualDeleteIndex(); + } + } + + internal class MenuItemTemplateSelector : DataTemplateSelector + { + public DataTemplate? NoteTemplate { get; set; } + public DataTemplate? DefaultTemplate { get; set; } + + protected override DataTemplate? SelectTemplateCore(object item) + { + return item is NoteViewModel ? NoteTemplate : DefaultTemplate; + } + } +} diff --git a/Samples/AppContentSearch/cs-winui/Models/Attachment.cs b/Samples/AppContentSearch/cs-winui/Models/Attachment.cs new file mode 100644 index 000000000..34c1f1d15 --- /dev/null +++ b/Samples/AppContentSearch/cs-winui/Models/Attachment.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +using System; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace Notes.Models +{ + public class Attachment : INotifyPropertyChanged + { + private bool isProcessed; + public int Id { get; set; } + public string? Filename { get; set; } + public string? FilenameForText { get; set; } + + public string? RelativePath { get; set; } + public NoteAttachmentType Type { get; set; } + public bool IsProcessed + { + get { return this.isProcessed; } + set + { + if (value != this.isProcessed) + { + this.isProcessed = value; + NotifyPropertyChanged(); + } + } + } + + public int NoteId { get; set; } + + public Note? Note { get; set; } + + private void NotifyPropertyChanged([CallerMemberName] String propertyName = "") + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + public event PropertyChangedEventHandler? PropertyChanged; + } + + public enum NoteAttachmentType + { + Image = 0, + Audio = 1, + Video = 2, + Document = 3 + } +} diff --git a/Samples/AppContentSearch/cs-winui/Models/Note.cs b/Samples/AppContentSearch/cs-winui/Models/Note.cs new file mode 100644 index 000000000..0bbcc0242 --- /dev/null +++ b/Samples/AppContentSearch/cs-winui/Models/Note.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +using System; +using System.Collections.Generic; + +namespace Notes.Models +{ + public class Note + { + public int Id { get; set; } + public string? Title { get; set; } + public string? Filename { get; set; } + public DateTime Created { get; set; } + public DateTime Modified { get; set; } + public List Attachments { get; set; } = new(); + } +} diff --git a/Samples/AppContentSearch/cs-winui/Notes.csproj b/Samples/AppContentSearch/cs-winui/Notes.csproj new file mode 100644 index 000000000..d43c76f47 --- /dev/null +++ b/Samples/AppContentSearch/cs-winui/Notes.csproj @@ -0,0 +1,149 @@ + + + WinExe + net8.0-windows10.0.26100.0 + Notes + app.manifest + x64;ARM64 + win-x86;win-x64;win-arm64 + enable + true + true + true + MVVMTK0045;CS8305 + true + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MSBuild:Compile + + + + + MSBuild:Compile + + + + + MSBuild:Compile + + + + + + true + + + + + False + False + True + enable + + + + + + + MSBuild:Compile + + + + + + + diff --git a/Samples/AppContentSearch/cs-winui/NotesApp.sln b/Samples/AppContentSearch/cs-winui/NotesApp.sln new file mode 100644 index 000000000..4886822de --- /dev/null +++ b/Samples/AppContentSearch/cs-winui/NotesApp.sln @@ -0,0 +1,26 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Notes", "Notes.csproj", "{DB0EC506-054D-44C7-50E7-440A96C593E2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|ARM64 = Debug|ARM64 + Release|ARM64 = Release|ARM64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DB0EC506-054D-44C7-50E7-440A96C593E2}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {DB0EC506-054D-44C7-50E7-440A96C593E2}.Debug|ARM64.Build.0 = Debug|ARM64 + {DB0EC506-054D-44C7-50E7-440A96C593E2}.Debug|ARM64.Deploy.0 = Debug|ARM64 + {DB0EC506-054D-44C7-50E7-440A96C593E2}.Release|ARM64.ActiveCfg = Release|ARM64 + {DB0EC506-054D-44C7-50E7-440A96C593E2}.Release|ARM64.Build.0 = Release|ARM64 + {DB0EC506-054D-44C7-50E7-440A96C593E2}.Release|ARM64.Deploy.0 = Release|ARM64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {EF3F85CE-E309-4547-838D-FBD37B2B6942} + EndGlobalSection +EndGlobal \ No newline at end of file diff --git a/Samples/AppContentSearch/cs-winui/Package.appxmanifest b/Samples/AppContentSearch/cs-winui/Package.appxmanifest new file mode 100644 index 000000000..c740d6551 --- /dev/null +++ b/Samples/AppContentSearch/cs-winui/Package.appxmanifest @@ -0,0 +1,53 @@ + + + + + + + + + + Notes + nikol + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Samples/AppContentSearch/cs-winui/Pages/NotesPage.xaml b/Samples/AppContentSearch/cs-winui/Pages/NotesPage.xaml new file mode 100644 index 000000000..62620d64c --- /dev/null +++ b/Samples/AppContentSearch/cs-winui/Pages/NotesPage.xaml @@ -0,0 +1,432 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +