Побудова " Lawier GPT " для вашого блогу - частина 5: Клієнт Windows (Українська (Ukrainian))

Побудова " Lawier GPT " для вашого блогу - частина 5: Клієнт Windows

Wednesday, 12 November 2025

//

16 minute read

ПОПЕРЕДЖЕННЯ: ТАКІ ПОСТА, ЯКІ ВАЖЛИВІ.

Це, ймовірно, багато з того, що нижче не спрацює, я генерую їх як як як як як-до мене, а потім роблю всі кроки і отримати зразок додатку працює... ви були підступні і бачили їх! вони ймовірно будуть готові до середини грудня.

## Вступ

Ласкаво просимо до П'ятої частиниМи побудували весь трубопровід.Частина 4), щоб зрозуміти вбудовування і векторний пошук (Частина 3), і щоб наша GPU була готова (Частина 2

Тепер настав час створити власний допоміжний інтерфейс для запису - клієнт Windows, у якому ви будете писати дописи блогу за допомогою пропозицій ШІ.ЗАУВАЖЕННЯ: це частина мого експерименту з комп'ютером (заступною чернеткою) + моїм власним редагуванням.Той самий голос, той самий прагматизм, тільки швидші пальці.Подумай проGitHub Copilot

або нові можливості комп' ютерного гравця у

VS код

- але для блогу.

У той час як ви друкуєте, система шукає ваші колишні дописи семантично і пропонує відповідні пропозиції, фрагменти коду та внутрішні зв'язки. |---------|-----|----------|------| | Вибір буфера інтерфейсу користувачаУ нас є три основних параметри побудови сучасної програми стільниці Windows у C#: | Порівняння кадрівДзвінок Авалоні | ПлатформаД-р Харріс: "Отже, це просто кос-платформа," | Дозрілість+WARE (2006) + 006) + New (222) | Підтримка XAMLName * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * | ШвидкодіяД_ д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. | Бібліотеки інтерфейсу користувачаТериторія (MaterialDesign, ModerWPF) } ДІТЕЙ

Крива навчання Mode}Після (якщо відомо WPF) = Mode}

Екосистема

  • Дівчино.
  • Мій вибір:
  • Авалоніяgreece_ prefectures. kgmЧому?
  • Кросплатформа (можна запустити на Linux/Mac, якщо потрібно)
  • Сучасний, активний

WPF

- Сумісний XML (простий для вивчення)

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

Хороша швидкодія:

  1. Зростання екосистемиАле ці концепції також стосуються WPF - я буду звертати увагу на альтернативи WPF, де вони відрізняються.
  2. Архітектура програмиКомпоненти ключів
  3. Головне вікно- Оболонка з меню, смужка стану
  4. Панель редактора- Там, де ви пишете (в редакторі markdown)
  5. Панель пропозицій- Потужні пропозиції комп' ютерного гравця

Панель пошуку

# 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

- Семантичний пошуковий інтерфейс

Шар служб

<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>

- Інтеграція з серверами

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();
    }
}

Налаштування проекту

Компонування головного вікна**Структура XML**MainWindowViewModel

Компонент редактора

<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>

Ми використаємо

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;
        }
    }
}

AvabonEdit

  • потужний компонент текстового редактора з підсвічуванням синтаксису.

РедакторView.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>

РедакторViewModel

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();
        }
    }
}

Панель пропозицій

Саме тут відбувається магія ШІ - показує семантичні подібні пости та пропозиції.

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);
    }
}

ПропозиціїView.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;
        }
    }
}

ПропозиціїViewModel

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);
}

Шар служб

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();
        }
    }
}

SplaceService inputation

dotnet run

Моделі

  • Налаштування виправлення залежностейApp.axaml.cs
  • Як запустити програмуВи повинні побачити:
  • Ліва панель: Редактор розміток на панелі інструментів і перегляд у реальному режимі

Права панель

: Подібні повідомлення базуються на тому, що ви пишете.

Смужка стану

: Кількість слів/ символів і перевірка обробки

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);
        });
    }
}

Як ви набираєте, панель пропозицій оновлюється у режимі реального часу з семантичним вмістом з вашого блогу!

Оптимізація швидкодії

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);
    }
}

Вирішальний пошук

Не шукати під час кожного натискання клавіші - зачекайте на паузу:

  1. Вбудовані каркасиВбудовування кешу для нещодавно введених контекстів:
  2. ✅ Markdown editor with live preview (Зведення)
  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

Ми побудували клієнт Windows з такими клієнтами:

Авалоніяgreece_ prefectures. kgm**Оболонка інтерфейсу користувача з шаблоном MVVM**AvabonEdit

  • Що далі?ВхідЧастина 6: Інтеграція локального LLM
  • Ми інтегруємо локальні підсумки LLM:
  • НалаштуванняLLamaSap
  • для локальної моделі виконання
  • Звантаження і квантажування моделей (Llama 2, Mistral)
  • Формат GGUF

і квантизацію пояснено

Підсумок на A4000 GPU

(це повідомлення)

Документація з AvaloniaAvaloniaEdit!

Finding related posts...
logo

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