diff --git a/BootstrapBlazor.Extensions.sln b/BootstrapBlazor.Extensions.sln index a9ddbde4..4948adcd 100644 --- a/BootstrapBlazor.Extensions.sln +++ b/BootstrapBlazor.Extensions.sln @@ -180,6 +180,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor.UniverSheet EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor.UniverIcon", "src\components\BootstrapBlazor.UniverIcon\BootstrapBlazor.UniverIcon.csproj", "{A657E04C-1495-439E-BC2E-1EEAB2D1B4DA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor.ChatBot", "src\components\BootstrapBlazor.ChatBot\BootstrapBlazor.ChatBot.csproj", "{2F37FBF4-5C1C-4493-B614-0E8361432621}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -486,6 +488,10 @@ Global {A657E04C-1495-439E-BC2E-1EEAB2D1B4DA}.Debug|Any CPU.Build.0 = Debug|Any CPU {A657E04C-1495-439E-BC2E-1EEAB2D1B4DA}.Release|Any CPU.ActiveCfg = Release|Any CPU {A657E04C-1495-439E-BC2E-1EEAB2D1B4DA}.Release|Any CPU.Build.0 = Release|Any CPU + {2F37FBF4-5C1C-4493-B614-0E8361432621}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F37FBF4-5C1C-4493-B614-0E8361432621}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F37FBF4-5C1C-4493-B614-0E8361432621}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F37FBF4-5C1C-4493-B614-0E8361432621}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -570,6 +576,7 @@ Global {F184D96E-7855-4E3B-B447-D09DBC1C91C6} = {FF1089BE-C704-4374-B629-C57C08E1798F} {E30AAB64-BF28-4960-89C1-1F521025F531} = {FF1089BE-C704-4374-B629-C57C08E1798F} {A657E04C-1495-439E-BC2E-1EEAB2D1B4DA} = {FF1089BE-C704-4374-B629-C57C08E1798F} + {2F37FBF4-5C1C-4493-B614-0E8361432621} = {FF1089BE-C704-4374-B629-C57C08E1798F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D5EB1960-6F30-4CE1-B375-EAE1F787D6FF} diff --git a/src/components/BootstrapBlazor.ChatBot/BootstrapBlazor.ChatBot.csproj b/src/components/BootstrapBlazor.ChatBot/BootstrapBlazor.ChatBot.csproj new file mode 100644 index 00000000..ee56aa54 --- /dev/null +++ b/src/components/BootstrapBlazor.ChatBot/BootstrapBlazor.ChatBot.csproj @@ -0,0 +1,26 @@ + + + + 9.0.0 + + + + Bootstrap Blazor WebAssembly wasm UI Components AI Chat bot + Bootstrap UI components extensions of ChatBot + BootstrapBlazor.Components + + + + + + + + + + + + + + + + diff --git a/src/components/BootstrapBlazor.ChatBot/MarkdownContent.razor b/src/components/BootstrapBlazor.ChatBot/MarkdownContent.razor new file mode 100644 index 00000000..91175e46 --- /dev/null +++ b/src/components/BootstrapBlazor.ChatBot/MarkdownContent.razor @@ -0,0 +1,17 @@ +@namespace BootstrapBlazor.Components +@inherits IdComponentBase + +
+ @((MarkupString)(_markdown ?? string.Empty)) +
+ + diff --git a/src/components/BootstrapBlazor.ChatBot/MarkdownContent.razor.cs b/src/components/BootstrapBlazor.ChatBot/MarkdownContent.razor.cs new file mode 100644 index 00000000..41b59c8f --- /dev/null +++ b/src/components/BootstrapBlazor.ChatBot/MarkdownContent.razor.cs @@ -0,0 +1,169 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using ColorCode; +using ColorCode.Compilation.Languages; +using ColorCode.Styling; +using Markdig; +using Markdown.ColorCode; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Logging; +using System.Text.RegularExpressions; + +namespace BootstrapBlazor.Components; + +/// +/// MarkdownContent component +/// +public partial class MarkdownContent +{ + /// + /// Gets or sets the content. Default value is null. + /// + [Parameter] + [EditorRequired] + public string? Content { get; set; } + + [Inject, NotNull] + private ILogger? Logger { get; set; } + + private string? _markdown; + private MarkdownPipeline? _markdownPipeline; + + /// + /// + /// + protected override void OnInitialized() + { + base.OnInitialized(); + + _markdownPipeline = new MarkdownPipelineBuilder() + .UsePipeTables() + .UseAdvancedExtensions() + .UseColorCode(styleDictionary: StyleDictionary.DefaultLight, additionalLanguages: new List() + { + new Json(), + new CSharp(), + new Cpp(), + new Css(), + new Html(), + new JavaScript(), + new Php(), + }) + .UseAutoLinks() + .UseEmojiAndSmiley() + .UseMediaLinks() + .UseCitations() + .UseMathematics() + .UseDiagrams() + .Build(); + } + + /// + /// + /// + protected override void OnParametersSet() + { + base.OnParametersSet(); + + _markdown = GetMarkdown(Content); + } + + private string GetMarkdown(string? toHtml) + { + var html = ""; + + if (string.IsNullOrEmpty(toHtml)) + { + return html; + } + + try + { + // 处理未封闭的 think 标签 + toHtml = HandleUnclosedThinkTags(toHtml); + + // 处理正常的 think 标签 + var thinkPattern = @"<\s*think\b[^>]*>(.*?)<\s*/\s*think\s*>"; + toHtml = Regex.Replace(toHtml, thinkPattern, @"
$1
", RegexOptions.Singleline | RegexOptions.IgnoreCase); + toHtml = RemoveEmbeddingsElement(toHtml); + + html = Markdig.Markdown.ToHtml(toHtml, _markdownPipeline); + var pattern = "(
(.*?)
)"; + var matches = Regex.Matches(html, pattern, RegexOptions.Singleline | RegexOptions.Multiline | RegexOptions.IgnoreCase); + + for (var i = matches.Count - 1; i >= 0; i--) + { + var match = matches[i].ToString(); + var id = "copy" + i; + var replacement = $"" + match; + html = html.Remove(matches[i].Index, matches[i].Length).Insert(matches[i].Index, replacement); + } + } + catch (Exception ex) + { + Logger.LogError(ex, "{GetMakrDown} method throw exception", nameof(GetMarkdown)); + } + + return html; + } + + private static string HandleUnclosedThinkTags(string content) + { + if (string.IsNullOrEmpty(content)) + return content; + + // 匹配开始标签 + var openTagPattern = @"<\s*think\b[^>]*>"; + var closeTagPattern = @"<\s*/\s*think\s*>"; + + var openTags = Regex.Matches(content, openTagPattern); + var closeTags = Regex.Matches(content, closeTagPattern); + + // 如果开始标签数量等于结束标签数量,说明标签都是配对的 + if (openTags.Count == closeTags.Count) + return content; + + // 处理未封闭的标签 + var parts = Regex.Split(content, openTagPattern); + if (parts.Length <= 1) + return content; + + var result = parts[0]; // 保留第一部分的内容 + for (int i = 1; i < parts.Length; i++) + { + var part = parts[i]; + // 检查这部分是否已经包含结束标签 + if (!part.Contains("", StringComparison.OrdinalIgnoreCase)) + { + // 没有结束标签,添加一个完整的 think 标签包装 + result += $"{part}"; + } + else + { + // 已经有结束标签,保持原样 + result += $"{part}"; + } + } + + return result; + } + + private static string RemoveEmbeddingsElement(string data) + { + if (string.IsNullOrEmpty(data)) + { + return ""; + } + string pattern = @"\[EMBEDDINGS\](.*?)\[/EMBEDDINGS\]"; + var matches = Regex.Matches(data, pattern, RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.IgnoreCase); + + if (matches.Count == 0) + { + return data; + } + + return Regex.Replace(data, pattern, "", RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.IgnoreCase); + } +} diff --git a/src/components/BootstrapBlazor.ChatBot/_Imports.razor b/src/components/BootstrapBlazor.ChatBot/_Imports.razor new file mode 100644 index 00000000..77285129 --- /dev/null +++ b/src/components/BootstrapBlazor.ChatBot/_Imports.razor @@ -0,0 +1 @@ +@using Microsoft.AspNetCore.Components.Web