Back to "Construire un "avocat GPT" pour votre blog - Partie 5: Le client Windows"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

AI AI-Article Avalonia C# LLM mostlylucid.blogllm WPF

Construire un "avocat GPT" pour votre blog - Partie 5: Le client Windows

Wednesday, 12 November 2025

AVERTISSEMENT: CES PROJETS DE POSTES QUI 'ESCAPÉ'.

Il est probable qu'une grande partie de ce qui est ci-dessous ne fonctionnera pas; je les génère comme comment faire pour MOI, puis faire toutes les étapes et obtenir le travail de l'application d'échantillon ... Vous avez été sournois et les a vus! ils seront probablement prêts à la mi-décembre.

## Présentation

Bienvenue dans la partie 5Nous avons construit tout le pipeline d'ingestion (Quatrième partie), comprendre l'intégration et la recherche vectorielle (Troisième partie) et que notre GPU soit prêt (Deuxième partie

Maintenant, il est temps de construire l'interface d'assistant d'écriture réelle - le client Windows où vous allez écrire des messages de blog avec des suggestions alimentées par l'IA.REMARQUE: Ceci fait partie de mes expériences avec l'IA (couture assistée) + mon propre montage.Même voix, même pragmatisme, juste des doigts plus rapides.Pensez àCopilote GitHub

ou les nouvelles fonctionnalités d'IA dans

Code VS

- mais pour écrire un blog.

Lorsque vous tapez, le système recherche vos messages passés de façon sémantique et offre des suggestions pertinentes, des extraits de code et des liens internes. |---------|-----|----------|------| | Choisir le cadre de l'assurance-chômageNous avons trois options principales pour construire une application de bureau Windows moderne dans C#: | Comparaison-cadreCaractéristiques WPFA Avalonia MAUI | PlateformeFenêtres uniquement. Plate-forme croisée. | ÉchéanceTrès mature (2006): Mature (2016): Nouveau (2022): | Assistance XAMLPlein (compatible) | RésultatsExcellente Excellente Bonne | Bibliothèques de l'assurance-chômageNombreux (MatérielDesign, ModernWPF)

Courbe d'apprentissageModéré Facile (si vous connaissez WPF) Modéré

Écosystèmes

  • Énorme de croissance
  • Mon choix :
  • AvaloniePourquoi ?
  • Plateforme croisée (peut fonctionner sous Linux/Mac si nécessaire)
  • Moderne, activement développée

Groupe de travail de la sécurité et de la sécurité au travail

-compatible XAML (facile à apprendre)

graph TB
    A[MainWindow] --> B[Editor Panel]
    A --> C[Suggestions Panel]
    A --> D[Search Panel]

    B --> E[AvalonEdit Component]
    B --> F[Markdown Preview]

    C --> G[Similar Posts List]
    C --> H[Code Snippets]
    C --> I[Suggested Links]

    D --> J[Semantic Search]
    D --> K[Keyword Filter]

    L[Services Layer] --> M[EmbeddingService]
    L --> N[VectorSearchService]
    L --> O[LLMService]

    B --> L
    C --> L
    D --> L

    class A mainWindow
    class B,C ui
    class L services

    classDef mainWindow stroke:#333,stroke-width:4px
    classDef ui stroke:#333,stroke-width:2px
    classDef services stroke:#333,stroke-width:2px

Bonne performance:

  1. Écosystèmes en pleine croissanceMais les concepts s'appliquent aussi au WPF - Je noterai les alternatives WPF là où elles diffèrent.
  2. Architecture de l'applicationComposantes clés
  3. MainWindow- Shell avec menu, barre d'état
  4. Panneau de l'éditeur- Où vous écrivez (rédacteur de notation)
  5. Groupe des suggestions- Suggestions alimentées par l'IA

Panneau de recherche

# Create Avalonia MVVM application
dotnet new install Avalonia.Templates
dotnet new avalonia.mvvm -n Mostlylucid.BlogLLM.Client

cd Mostlylucid.BlogLLM.Client

# Add necessary packages (latest versions)
dotnet add package AvaloniaEdit
dotnet add package Markdown.Avalonia
dotnet add package CommunityToolkit.Mvvm

# Add references to our core library
dotnet add reference ../Mostlylucid.BlogLLM.Core

- Interface de recherche sémantique

Couche de services

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="using:Mostlylucid.BlogLLM.Client.ViewModels"
        xmlns:views="using:Mostlylucid.BlogLLM.Client.Views"
        x:Class="Mostlylucid.BlogLLM.Client.Views.MainWindow"
        x:DataType="vm:MainWindowViewModel"
        Title="Blog Writing Assistant"
        Width="1400" Height="900">

    <Design.DataContext>
        <vm:MainWindowViewModel />
    </Design.DataContext>

    <Grid RowDefinitions="Auto,*,Auto">
        <!-- Menu Bar -->
        <Menu Grid.Row="0">
            <MenuItem Header="_File">
                <MenuItem Header="_New Post" Command="{Binding NewPostCommand}" />
                <MenuItem Header="_Open Post" Command="{Binding OpenPostCommand}" />
                <MenuItem Header="_Save" Command="{Binding SaveCommand}" />
                <Separator />
                <MenuItem Header="E_xit" Command="{Binding ExitCommand}" />
            </MenuItem>
            <MenuItem Header="_Edit">
                <MenuItem Header="_Undo" Command="{Binding UndoCommand}" />
                <MenuItem Header="_Redo" Command="{Binding RedoCommand}" />
            </MenuItem>
            <MenuItem Header="_AI">
                <MenuItem Header="_Generate Suggestions" Command="{Binding GenerateSuggestionsCommand}" />
                <MenuItem Header="_Semantic Search" Command="{Binding OpenSearchCommand}" />
                <MenuItem Header="_Insert Link" Command="{Binding InsertLinkCommand}" />
            </MenuItem>
        </Menu>

        <!-- Main Content - Split View -->
        <Grid Grid.Row="1" ColumnDefinitions="2*,*">
            <!-- Left: Editor -->
            <Border Grid.Column="0" BorderBrush="LightGray" BorderThickness="0,0,1,0">
                <views:EditorView DataContext="{Binding EditorViewModel}" />
            </Border>

            <!-- Right: Suggestions -->
            <Border Grid.Column="1">
                <views:SuggestionsView DataContext="{Binding SuggestionsViewModel}" />
            </Border>
        </Grid>

        <!-- Status Bar -->
        <Border Grid.Row="2" Background="WhiteSmoke" Padding="8,4">
            <Grid ColumnDefinitions="*,Auto,Auto,Auto">
                <TextBlock Grid.Column="0" Text="{Binding StatusMessage}" />
                <TextBlock Grid.Column="1" Text="{Binding WordCount, StringFormat='Words: {0}'}" Margin="10,0" />
                <TextBlock Grid.Column="2" Text="{Binding CharCount, StringFormat='Chars: {0}'}" Margin="10,0" />
                <ProgressBar Grid.Column="3"
                             Width="100"
                             Height="16"
                             IsVisible="{Binding IsProcessing}"
                             IsIndeterminate="True" />
            </Grid>
        </Border>
    </Grid>
</Window>

- Intégration de l'arrière-plan

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Mostlylucid.BlogLLM.Client.Services;
using System.Threading.Tasks;

namespace Mostlylucid.BlogLLM.Client.ViewModels
{
    public partial class MainWindowViewModel : ViewModelBase
    {
        private readonly IEditorService _editorService;
        private readonly ISuggestionService _suggestionService;

        [ObservableProperty]
        private EditorViewModel _editorViewModel;

        [ObservableProperty]
        private SuggestionsViewModel _suggestionsViewModel;

        [ObservableProperty]
        private string _statusMessage = "Ready";

        [ObservableProperty]
        private int _wordCount;

        [ObservableProperty]
        private int _charCount;

        [ObservableProperty]
        private bool _isProcessing;

        public MainWindowViewModel(
            IEditorService editorService,
            ISuggestionService suggestionService)
        {
            _editorService = editorService;
            _suggestionService = suggestionService;

            EditorViewModel = new EditorViewModel(editorService);
            SuggestionsViewModel = new SuggestionsViewModel(suggestionService);

            // Subscribe to editor changes
            EditorViewModel.PropertyChanged += (s, e) =>
            {
                if (e.PropertyName == nameof(EditorViewModel.Text))
                {
                    UpdateStatistics();
                    _ = GenerateSuggestionsAsync();
                }
            };
        }

        [RelayCommand]
        private async Task NewPost()
        {
            EditorViewModel.Text = GenerateNewPostTemplate();
            StatusMessage = "New post created";
        }

        [RelayCommand]
        private async Task OpenPost()
        {
            var dialog = new OpenFileDialog
            {
                Filters = new List<FileDialogFilter>
                {
                    new FileDialogFilter { Name = "Markdown", Extensions = { "md" } }
                }
            };

            var result = await dialog.ShowAsync(GetMainWindow());
            if (result != null && result.Length > 0)
            {
                EditorViewModel.Text = await File.ReadAllTextAsync(result[0]);
                StatusMessage = $"Opened: {Path.GetFileName(result[0])}";
            }
        }

        [RelayCommand]
        private async Task Save()
        {
            var dialog = new SaveFileDialog
            {
                Filters = new List<FileDialogFilter>
                {
                    new FileDialogFilter { Name = "Markdown", Extensions = { "md" } }
                },
                DefaultExtension = "md"
            };

            var result = await dialog.ShowAsync(GetMainWindow());
            if (!string.IsNullOrEmpty(result))
            {
                await File.WriteAllTextAsync(result, EditorViewModel.Text);
                StatusMessage = $"Saved: {Path.GetFileName(result)}";
            }
        }

        [RelayCommand]
        private async Task GenerateSuggestions()
        {
            IsProcessing = true;
            StatusMessage = "Generating suggestions...";

            try
            {
                var currentText = EditorViewModel.Text;
                await SuggestionsViewModel.GenerateSuggestionsAsync(currentText);
                StatusMessage = "Suggestions generated";
            }
            catch (Exception ex)
            {
                StatusMessage = $"Error: {ex.Message}";
            }
            finally
            {
                IsProcessing = false;
            }
        }

        private void UpdateStatistics()
        {
            var text = EditorViewModel.Text ?? string.Empty;
            CharCount = text.Length;
            WordCount = text.Split(new[] { ' ', '\n', '\r', '\t' },
                StringSplitOptions.RemoveEmptyEntries).Length;
        }

        private string GenerateNewPostTemplate()
        {
            var today = DateTime.Now.ToString("yyyy-MM-ddTHH:mm");
            return $@"# New Blog Post


<datetime class=""hidden"">{today}</datetime>

## Introduction

Write your introduction here...

[TOC]

## Section 1

Content here...
";
        }

        private Window GetMainWindow() =>
            (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)
                ?.MainWindow ?? throw new InvalidOperationException();
    }
}

Mise en place du projet

Mise en page de la fenêtre principale**Structure XAML**Modèle MainWindowView

Composant de l'éditeur

<UserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:avalonEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit"
             x:Class="Mostlylucid.BlogLLM.Client.Views.EditorView">

    <Grid RowDefinitions="Auto,*,*">
        <!-- Toolbar -->
        <StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="5" Margin="5">
            <Button Content="Bold" Command="{Binding InsertBoldCommand}" />
            <Button Content="Italic" Command="{Binding InsertItalicCommand}" />
            <Button Content="Code" Command="{Binding InsertCodeCommand}" />
            <Separator />
            <Button Content="H1" Command="{Binding InsertHeadingCommand}" CommandParameter="1" />
            <Button Content="H2" Command="{Binding InsertHeadingCommand}" CommandParameter="2" />
            <Button Content="H3" Command="{Binding InsertHeadingCommand}" CommandParameter="3" />
            <Separator />
            <Button Content="Link" Command="{Binding InsertLinkCommand}" />
            <Button Content="Image" Command="{Binding InsertImageCommand}" />
        </StackPanel>

        <!-- Editor -->
        <avalonEdit:TextEditor Grid.Row="1"
                               Name="Editor"
                               FontFamily="Consolas,Courier New"
                               FontSize="14"
                               ShowLineNumbers="True"
                               WordWrap="True"
                               Document="{Binding Document}"
                               SyntaxHighlighting="MarkDown" />

        <!-- Live Preview -->
        <Border Grid.Row="2" BorderBrush="LightGray" BorderThickness="0,1,0,0">
            <ScrollViewer>
                <MarkdownScrollViewer Markdown="{Binding Text}"
                                    Margin="10"
                                    Background="White" />
            </ScrollViewer>
        </Border>
    </Grid>
</UserControl>

Nous utiliserons

using AvaloniaEdit.Document;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace Mostlylucid.BlogLLM.Client.ViewModels
{
    public partial class EditorViewModel : ViewModelBase
    {
        private readonly IEditorService _editorService;

        [ObservableProperty]
        private TextDocument _document = new();

        [ObservableProperty]
        private string _text = string.Empty;

        [ObservableProperty]
        private int _caretOffset;

        public EditorViewModel(IEditorService editorService)
        {
            _editorService = editorService;

            // Sync Document and Text
            Document.TextChanged += (s, e) =>
            {
                Text = Document.Text;
            };
        }

        [RelayCommand]
        private void InsertBold()
        {
            InsertMarkdownWrapper("**", "**", "bold text");
        }

        [RelayCommand]
        private void InsertItalic()
        {
            InsertMarkdownWrapper("*", "*", "italic text");
        }

        [RelayCommand]
        private void InsertCode()
        {
            InsertMarkdownWrapper("`", "`", "code");
        }

        [RelayCommand]
        private void InsertHeading(string level)
        {
            var headingMarker = new string('#', int.Parse(level));
            Document.Insert(CaretOffset, $"{headingMarker} Heading {level}\n");
        }

        [RelayCommand]
        private void InsertLink()
        {
            InsertMarkdownWrapper("[", "](url)", "link text");
        }

        [RelayCommand]
        private void InsertImage()
        {
            Document.Insert(CaretOffset, "![alt text](image-url.png)");
        }

        private void InsertMarkdownWrapper(string before, string after, string placeholder)
        {
            var selectedText = GetSelectedText();

            if (string.IsNullOrEmpty(selectedText))
            {
                Document.Insert(CaretOffset, $"{before}{placeholder}{after}");
            }
            else
            {
                var selectionStart = Document.GetOffset(Document.GetLocation(CaretOffset));
                Document.Replace(selectionStart, selectedText.Length, $"{before}{selectedText}{after}");
            }
        }

        private string GetSelectedText()
        {
            // This would get actual selection from AvalonEdit
            // Simplified for example
            return string.Empty;
        }
    }
}

AvalonEdit

  • un puissant composant d'éditeur de texte avec mise en évidence syntaxique.

EditorView.axaml

<UserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             x:Class="Mostlylucid.BlogLLM.Client.Views.SuggestionsView">

    <TabControl>
        <!-- Similar Posts -->
        <TabItem Header="Similar Posts">
            <Grid RowDefinitions="Auto,*">
                <StackPanel Grid.Row="0" Margin="5">
                    <TextBlock Text="Related content from your blog:" FontWeight="Bold" />
                    <TextBlock Text="{Binding SimilarPostsCount, StringFormat='{0} posts found'}"
                               FontSize="11" Foreground="Gray" />
                </StackPanel>

                <ListBox Grid.Row="1"
                         ItemsSource="{Binding SimilarPosts}"
                         SelectedItem="{Binding SelectedPost}">
                    <ListBox.ItemTemplate>
                        <DataTemplate>
                            <Border BorderBrush="LightGray"
                                    BorderThickness="1"
                                    Padding="8"
                                    Margin="4"
                                    CornerRadius="4">
                                <StackPanel>
                                    <TextBlock Text="{Binding Title}"
                                               FontWeight="Bold"
                                               TextWrapping="Wrap" />
                                    <TextBlock Text="{Binding SectionHeading}"
                                               FontSize="11"
                                               Foreground="DarkBlue"
                                               Margin="0,2" />
                                    <TextBlock Text="{Binding Preview}"
                                               TextWrapping="Wrap"
                                               MaxHeight="60"
                                               FontSize="12"
                                               Margin="0,4" />
                                    <StackPanel Orientation="Horizontal" Spacing="10" Margin="0,4,0,0">
                                        <TextBlock Text="{Binding Score, StringFormat='Similarity: {0:P0}'}"
                                                   FontSize="11"
                                                   Foreground="Green" />
                                        <Button Content="Insert Link"
                                                Command="{Binding $parent[ListBox].DataContext.InsertLinkCommand}"
                                                CommandParameter="{Binding}"
                                                FontSize="11" />
                                        <Button Content="View"
                                                Command="{Binding $parent[ListBox].DataContext.ViewPostCommand}"
                                                CommandParameter="{Binding}"
                                                FontSize="11" />
                                    </StackPanel>
                                </StackPanel>
                            </Border>
                        </DataTemplate>
                    </ListBox.ItemTemplate>
                </ListBox>
            </Grid>
        </TabItem>

        <!-- Code Snippets -->
        <TabItem Header="Code Snippets">
            <Grid RowDefinitions="Auto,*">
                <TextBlock Grid.Row="0"
                           Text="Relevant code from past posts:"
                           FontWeight="Bold"
                           Margin="5" />

                <ListBox Grid.Row="1" ItemsSource="{Binding CodeSnippets}">
                    <ListBox.ItemTemplate>
                        <DataTemplate>
                            <Border BorderBrush="LightGray"
                                    BorderThickness="1"
                                    Padding="8"
                                    Margin="4"
                                    Background="WhiteSmoke">
                                <StackPanel>
                                    <TextBlock Text="{Binding Language}"
                                               FontFamily="Consolas"
                                               FontSize="11"
                                               Foreground="DarkGray" />
                                    <TextBlock Text="{Binding Code}"
                                               FontFamily="Consolas"
                                               TextWrapping="Wrap"
                                               Margin="0,4" />
                                    <Button Content="Insert"
                                            Command="{Binding $parent[ListBox].DataContext.InsertCodeCommand}"
                                            CommandParameter="{Binding}"
                                            HorizontalAlignment="Right" />
                                </StackPanel>
                            </Border>
                        </DataTemplate>
                    </ListBox.ItemTemplate>
                </ListBox>
            </Grid>
        </TabItem>

        <!-- AI Suggestions -->
        <TabItem Header="AI Suggestions">
            <Grid RowDefinitions="Auto,*,Auto">
                <TextBlock Grid.Row="0"
                           Text="AI-generated suggestions:"
                           FontWeight="Bold"
                           Margin="5" />

                <ScrollViewer Grid.Row="1">
                    <TextBlock Text="{Binding AiSuggestion}"
                               TextWrapping="Wrap"
                               Margin="10"
                               FontSize="13" />
                </ScrollViewer>

                <Button Grid.Row="2"
                        Content="Generate New Suggestion"
                        Command="{Binding RegenerateSuggestionCommand}"
                        Margin="5" />
            </Grid>
        </TabItem>
    </TabControl>
</UserControl>

Modèle d'affichage de l'éditeur

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Mostlylucid.BlogLLM.Client.Models;
using Mostlylucid.BlogLLM.Client.Services;
using System.Collections.ObjectModel;

namespace Mostlylucid.BlogLLM.Client.ViewModels
{
    public partial class SuggestionsViewModel : ViewModelBase
    {
        private readonly ISuggestionService _suggestionService;

        [ObservableProperty]
        private ObservableCollection<SimilarPost> _similarPosts = new();

        [ObservableProperty]
        private ObservableCollection<CodeSnippet> _codeSnippets = new();

        [ObservableProperty]
        private string _aiSuggestion = string.Empty;

        [ObservableProperty]
        private SimilarPost? _selectedPost;

        [ObservableProperty]
        private int _similarPostsCount;

        public SuggestionsViewModel(ISuggestionService suggestionService)
        {
            _suggestionService = suggestionService;
        }

        public async Task GenerateSuggestionsAsync(string currentText)
        {
            // Extract last few sentences as context
            var context = ExtractContext(currentText);

            // Search for similar posts
            var similarPosts = await _suggestionService.FindSimilarPostsAsync(context);
            SimilarPosts.Clear();
            foreach (var post in similarPosts)
            {
                SimilarPosts.Add(post);
            }
            SimilarPostsCount = SimilarPosts.Count;

            // Extract code snippets from similar posts
            var codeSnippets = await _suggestionService.ExtractCodeSnippetsAsync(similarPosts);
            CodeSnippets.Clear();
            foreach (var snippet in codeSnippets)
            {
                CodeSnippets.Add(snippet);
            }

            // Generate AI suggestion (we'll implement this in Part 6)
            // AiSuggestion = await _suggestionService.GenerateAiSuggestionAsync(currentText, similarPosts);
        }

        [RelayCommand]
        private void InsertLink(SimilarPost post)
        {
            var link = $"[{post.Title}](/blog/{post.Slug}#{post.SectionHeading.ToLower().Replace(" ", "-")})";
            // Notify EditorViewModel to insert link
            WeakReferenceMessenger.Default.Send(new InsertTextMessage(link));
        }

        [RelayCommand]
        private void ViewPost(SimilarPost post)
        {
            // Open in browser
            var url = $"https://www.mostlylucid.net/blog/{post.Slug}";
            Process.Start(new ProcessStartInfo { FileName = url, UseShellExecute = true });
        }

        [RelayCommand]
        private void InsertCode(CodeSnippet snippet)
        {
            var code = $"```{snippet.Language}\n{snippet.Code}\n```";
            WeakReferenceMessenger.Default.Send(new InsertTextMessage(code));
        }

        [RelayCommand]
        private async Task RegenerateSuggestion()
        {
            // To be implemented in Part 6 with LLM integration
            AiSuggestion = "AI suggestions will be available in Part 6...";
        }

        private string ExtractContext(string text, int sentences = 3)
        {
            // Get last N sentences as context for search
            var sentenceEndings = new[] { '.', '!', '?' };
            var sentences_found = 0;
            var index = text.Length - 1;

            while (index >= 0 && sentences_found < sentences)
            {
                if (sentenceEndings.Contains(text[index]))
                {
                    sentences_found++;
                }
                index--;
            }

            return index < 0 ? text : text.Substring(index + 1).Trim();
        }
    }
}

Groupe des suggestions

C'est là que se produit la magie de l'IA - montrant sémantiquement des messages et des suggestions similaires.

using Mostlylucid.BlogLLM.Client.Models;

namespace Mostlylucid.BlogLLM.Client.Services
{
    public interface ISuggestionService
    {
        Task<List<SimilarPost>> FindSimilarPostsAsync(string context);
        Task<List<CodeSnippet>> ExtractCodeSnippetsAsync(List<SimilarPost> posts);
        Task<string> GenerateAiSuggestionAsync(string currentText, List<SimilarPost> context);
    }
}

SuggestionsView.axaml

using Mostlylucid.BlogLLM.Client.Models;
using Mostlylucid.BlogLLM.Core.Services;

namespace Mostlylucid.BlogLLM.Client.Services
{
    public class SuggestionService : ISuggestionService
    {
        private readonly BatchEmbeddingService _embeddingService;
        private readonly QdrantVectorStore _vectorStore;

        public SuggestionService(
            BatchEmbeddingService embeddingService,
            QdrantVectorStore vectorStore)
        {
            _embeddingService = embeddingService;
            _vectorStore = vectorStore;
        }

        public async Task<List<SimilarPost>> FindSimilarPostsAsync(string context)
        {
            // Generate embedding for context
            var embedding = _embeddingService.GenerateEmbedding(context);

            // Search vector database
            var results = await _vectorStore.SearchAsync(
                queryEmbedding: embedding,
                limit: 10,
                languageFilter: "en"
            );

            // Convert to SimilarPost models
            return results.Select(r => new SimilarPost
            {
                Slug = r.BlogPostSlug,
                Title = r.BlogPostTitle,
                SectionHeading = r.SectionHeading,
                Preview = r.Text.Length > 200 ? r.Text.Substring(0, 200) + "..." : r.Text,
                FullText = r.Text,
                Score = r.Score
            }).ToList();
        }

        public async Task<List<CodeSnippet>> ExtractCodeSnippetsAsync(List<SimilarPost> posts)
        {
            var snippets = new List<CodeSnippet>();

            foreach (var post in posts)
            {
                // Extract code blocks from markdown
                var codeBlocks = ExtractCodeBlocks(post.FullText);
                snippets.AddRange(codeBlocks);
            }

            // Deduplicate and return top 5
            return snippets
                .GroupBy(s => s.Code)
                .Select(g => g.First())
                .Take(5)
                .ToList();
        }

        public async Task<string> GenerateAiSuggestionAsync(string currentText, List<SimilarPost> context)
        {
            // This will be implemented in Part 6 with LLM integration
            await Task.CompletedTask;
            return "AI generation coming in Part 6...";
        }

        private List<CodeSnippet> ExtractCodeBlocks(string markdown)
        {
            var snippets = new List<CodeSnippet>();
            var regex = new Regex(@"```(\w+)\n(.*?)\n```", RegexOptions.Singleline);
            var matches = regex.Matches(markdown);

            foreach (Match match in matches)
            {
                snippets.Add(new CodeSnippet
                {
                    Language = match.Groups[1].Value,
                    Code = match.Groups[2].Value.Trim()
                });
            }

            return snippets;
        }
    }
}

SuggestionsVoirModèle

namespace Mostlylucid.BlogLLM.Client.Models
{
    public class SimilarPost
    {
        public string Slug { get; set; } = string.Empty;
        public string Title { get; set; } = string.Empty;
        public string SectionHeading { get; set; } = string.Empty;
        public string Preview { get; set; } = string.Empty;
        public string FullText { get; set; } = string.Empty;
        public float Score { get; set; }
    }

    public class CodeSnippet
    {
        public string Language { get; set; } = string.Empty;
        public string Code { get; set; } = string.Empty;
    }

    public record InsertTextMessage(string Text);
}

Couche de services

ISuggestionService

using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Microsoft.Extensions.DependencyInjection;
using Mostlylucid.BlogLLM.Client.Services;
using Mostlylucid.BlogLLM.Client.ViewModels;
using Mostlylucid.BlogLLM.Client.Views;
using Mostlylucid.BlogLLM.Core.Services;

namespace Mostlylucid.BlogLLM.Client
{
    public partial class App : Application
    {
        public IServiceProvider Services { get; private set; } = null!;

        public override void Initialize()
        {
            AvaloniaXamlLoader.Load(this);
        }

        public override void OnFrameworkInitializationCompleted()
        {
            // Setup DI
            var services = new ServiceCollection();

            // Register core services
            services.AddSingleton(_ => new BatchEmbeddingService(
                modelPath: "C:\\models\\bge-base-en-onnx\\model.onnx",
                tokenizerPath: "C:\\models\\bge-base-en-onnx\\tokenizer.json",
                useGpu: true
            ));

            services.AddSingleton(_ => new QdrantVectorStore(
                host: "localhost",
                port: 6334
            ));

            // Register app services
            services.AddSingleton<IEditorService, EditorService>();
            services.AddSingleton<ISuggestionService, SuggestionService>();

            // Register ViewModels
            services.AddTransient<MainWindowViewModel>();
            services.AddTransient<EditorViewModel>();
            services.AddTransient<SuggestionsViewModel>();

            Services = services.BuildServiceProvider();

            // Create main window
            if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
            {
                desktop.MainWindow = new MainWindow
                {
                    DataContext = Services.GetRequiredService<MainWindowViewModel>()
                };
            }

            base.OnFrameworkInitializationCompleted();
        }
    }
}

Mise en œuvre du service Suggestion

dotnet run

Modèles

  • Configuration de l'injection de dépendanceApp.axaml.cs
  • Exécution de la demandeVous devriez voir:
  • Panneau gauche: Éditeur Markdown avec barre d'outils et aperçu en direct

Panneau droit

: Articles similaires basés sur ce que vous écrivez

Barre d' état

: Nombre de mots/caractères et indicateur de traitement

public partial class EditorViewModel : ViewModelBase
{
    private System.Timers.Timer _searchDebounceTimer;
    private const int DebounceMs = 500;

    public EditorViewModel(IEditorService editorService)
    {
        _editorService = editorService;

        _searchDebounceTimer = new System.Timers.Timer(DebounceMs);
        _searchDebounceTimer.AutoReset = false;
        _searchDebounceTimer.Elapsed += async (s, e) =>
        {
            await GenerateSuggestionsAsync();
        };

        Document.TextChanged += (s, e) =>
        {
            Text = Document.Text;

            // Restart debounce timer
            _searchDebounceTimer.Stop();
            _searchDebounceTimer.Start();
        };
    }

    private async Task GenerateSuggestionsAsync()
    {
        var context = ExtractContext(Text);
        await Dispatcher.UIThread.InvokeAsync(async () =>
        {
            await SuggestionsViewModel.GenerateSuggestionsAsync(context);
        });
    }
}

Au fur et à mesure que vous tapez, le panneau de suggestions met à jour en temps réel avec du contenu sémantiquement similaire de votre blog!

Optimisation des performances

public class EmbeddingCache
{
    private readonly Dictionary<string, (float[] embedding, DateTime timestamp)> _cache = new();
    private const int MaxCacheSize = 100;
    private readonly TimeSpan _cacheExpiration = TimeSpan.FromMinutes(10);

    public bool TryGet(string text, out float[] embedding)
    {
        if (_cache.TryGetValue(text, out var cached))
        {
            if (DateTime.Now - cached.timestamp < _cacheExpiration)
            {
                embedding = cached.embedding;
                return true;
            }
            _cache.Remove(text);
        }

        embedding = null;
        return false;
    }

    public void Set(string text, float[] embedding)
    {
        if (_cache.Count >= MaxCacheSize)
        {
            // Remove oldest
            var oldest = _cache.OrderBy(kv => kv.Value.timestamp).First();
            _cache.Remove(oldest.Key);
        }

        _cache[text] = (embedding, DateTime.Now);
    }
}

Dénonciation de la recherche

Ne pas chercher sur chaque frappe - attendre la pause:

  1. Embedures de cacheIntégrer les caches pour les contextes dactylographiés récemment :
  2. ✅ Markdown editor with live preview (Résumé)
  3. ✅ Real-time semantic search as you type
  4. ✅ Suggestions panel showing similar posts
  5. ✅ Code snippet extraction and insertion
  6. ✅ Link generation to related posts
  7. ✅ Debounced search for performance
  8. ✅ Dependency injection architecture

Nous avons construit un client Windows complet avec:

Avalonie**Cadre d'interface utilisateur avec modèle MVVM**AvalonEdit

  • Qu'est-ce qu'il y a ?DansPartie 6: Intégration locale des LLM
  • , nous intégrerons l'inférence locale LLM:
  • Mise en placeLLamaSharp
  • pour l'exécution de modèles locaux
  • Téléchargement et quantification des modèles (Llama 2, Mistral)
  • Format GGUF

et quantification expliquées

Inférence en cours d'exécution sur A4000 GPU

(ce poste)

Documentation sur l'avaloniaÉdition Avalonia!

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.