Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions BootstrapBlazor.Extensions.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<Version>9.0.0</Version>
</PropertyGroup>

<PropertyGroup>
<PackageTags>Bootstrap Blazor WebAssembly wasm UI Components AI Chat bot</PackageTags>
<Description>Bootstrap UI components extensions of ChatBot</Description>
<RootNamespace>BootstrapBlazor.Components</RootNamespace>
</PropertyGroup>

<ItemGroup>
<SupportedPlatform Include="browser" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Markdig" Version="0.40.0" />
<PackageReference Include="Markdown.ColorCode" Version="3.0.0" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="BootstrapBlazor" Version="$(BBVersion)" />
</ItemGroup>

</Project>
17 changes: 17 additions & 0 deletions src/components/BootstrapBlazor.ChatBot/MarkdownContent.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@namespace BootstrapBlazor.Components
@inherits IdComponentBase

<div id="@Id">
@((MarkupString)(_markdown ?? string.Empty))
</div>

<style>
.think {
margin-bottom: 1rem;
font-size: 0.75rem;
font-weight: 400;
color: #A0A0A0;
border-left: 3px solid #dfdbdb;
padding-left: 10px;
}
</style>
169 changes: 169 additions & 0 deletions src/components/BootstrapBlazor.ChatBot/MarkdownContent.razor.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// MarkdownContent component
/// </summary>
public partial class MarkdownContent
{
/// <summary>
/// Gets or sets the content. Default value is null.
/// </summary>
[Parameter]
[EditorRequired]
public string? Content { get; set; }

[Inject, NotNull]
private ILogger<MarkdownContent>? Logger { get; set; }

private string? _markdown;
private MarkdownPipeline? _markdownPipeline;

/// <summary>
/// <inheritdoc/>
/// </summary>
protected override void OnInitialized()
{
base.OnInitialized();

_markdownPipeline = new MarkdownPipelineBuilder()
.UsePipeTables()
.UseAdvancedExtensions()
.UseColorCode(styleDictionary: StyleDictionary.DefaultLight, additionalLanguages: new List<ILanguage>()
{
new Json(),
new CSharp(),
new Cpp(),
new Css(),
new Html(),
new JavaScript(),
new Php(),
})
.UseAutoLinks()
.UseEmojiAndSmiley()
.UseMediaLinks()
.UseCitations()
.UseMathematics()
.UseDiagrams()
.Build();
}

/// <summary>
/// <inheritdoc/>
/// </summary>
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, @"<div class=""think"">$1</div>", RegexOptions.Singleline | RegexOptions.IgnoreCase);
toHtml = RemoveEmbeddingsElement(toHtml);

html = Markdig.Markdown.ToHtml(toHtml, _markdownPipeline);
var pattern = "(<div style=\"color:#DADADA;background-color:#1E1E1E;\"><pre>(.*?)</pre></div>)";
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 = $"<button data-clipboard-target=\"#{id}\" class=\"float-end copyBtn mt-0\">Copy</button>" + 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));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (typo): Fix typo in error log placeholder.

The placeholder '{GetMakrDown}' seems to be a typo. It would be clearer and more consistent to use '{GetMarkdown}' in order to accurately reflect the method name.

Suggested change
Logger.LogError(ex, "{GetMakrDown} method throw exception", nameof(GetMarkdown));
Logger.LogError(ex, "{GetMarkdown} method throw exception", nameof(GetMarkdown));

}

return html;
}

private static string HandleUnclosedThinkTags(string content)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider using an HTML parser library like HtmlAgilityPack to handle unclosed "think" tags instead of regex and string manipulation.

One way to reduce the “magic” of nested regex and manual string concatenation is to encapsulate the “think” tag repair logic into a small helper that uses a lightweight HTML parser rather than ad hoc regex-splitting. For example, you can replace your current implementation with one based on [HtmlAgilityPack](https://html-agility-pack.net/) to “repair” unclosed tags. This makes the intent explicit and the code easier to maintain.

For example, you might create a helper like this:

```csharp
using HtmlAgilityPack;

private static string FixThinkTags(string content)
{
    if (string.IsNullOrEmpty(content)) return content;

    // Load the content into an HTML document
    var doc = new HtmlDocument();
    doc.OptionFixNestedTags = true;
    doc.LoadHtml(content);

    // Select all nodes with the "think" tag.
    // (HtmlAgilityPack will auto-close unclosed tags on load)
    var thinkNodes = doc.DocumentNode.SelectNodes("//think");
    if (thinkNodes != null)
    {
        foreach (var node in thinkNodes)
        {
            // Optionally, further manipulate the node here if needed...
            // e.g. add a div wrapper if that was your intent when converting via regex
            var div = HtmlNode.CreateNode("<div class=\"think\"></div>");
            div.InnerHtml = node.InnerHtml;
            node.ParentNode.ReplaceChild(div, node);
        }
    }
    return doc.DocumentNode.OuterHtml;
}

Then in your GetMarkdown method replace the call to HandleUnclosedThinkTags with:

toHtml = FixThinkTags(toHtml);

This refactoring does not remove any functionality but delegates tag fixing to a dedicated parser which is easier to understand, unit test, and maintain. If you do not want to add a dependency now, consider extracting the regex/string-splitting logic into a separate method with clear inline documentation representing a “repair” step. Either way, encapsulating this complexity into a focused helper reduces the maintenance burden.

{
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("</think>", StringComparison.OrdinalIgnoreCase))
{
// 没有结束标签,添加一个完整的 think 标签包装
result += $"<think>{part}</think>";
}
else
{
// 已经有结束标签,保持原样
result += $"<think>{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);
}
}
1 change: 1 addition & 0 deletions src/components/BootstrapBlazor.ChatBot/_Imports.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@using Microsoft.AspNetCore.Components.Web