# 2024 年 10 月 1 日

接入实际的 llamacpp 根据配置的调用使用,重构了一波异步计算信息,将简单的快捷解释界面基本弄好了

# 2024 年 10 月 2 日

今天主要是优化了各个界面排版,特别是聊天界面整体都重构了下。

还有之前感觉在 UI 涉及数据更新的时候卡顿问题,最后加了更新延迟:特别是 LLM 流式传输,打字机效果一样显示的情况,调整了 UI 更新频率感觉好些了。

# 2024 年 10 月 3 日

搞了一天加 Markdown 渲染效果,开始以为很简单,头一天晚上都想好了:

  • 先用 Markdig 配合 Markdown.ColorCode.CSharpToColoredHtml 渲染成 HTML
  • 然后使用 Avalonia.HtmlRenderer 渲染显示

怎么样?想着是不是很简单呢?

结果... 先是发现显示很有问题:Avalonia.HtmlRenderer 显示起来,感觉很怪,跟现在的主题完全对不上号。其次渲染感觉也有问题。其次流式更新性能也很糟糕....

然后中途就查另外一个 Avalonia.Markdown,换成这个也有不少坑,比如代码高亮都没有,最后到处查,查 issue,结果最后在它自己的 wiki... 最后一篇才写着:代码用的 TextEditor,得 Fluent 主题才支持!

于是又研究来研究去的... 最后得出结论:

  • 可以给 MarkdownScrollViewer 指定 FluentTheme,然后内联指定 SimpleTheme 配合 avares://AvaloniaEdit/Themes/Simple/AvaloniaEdit.xaml 勉强正常显示
  • 之所以说勉强,是因为有些显示不对:文字选择、代码高亮都没有了
  • 代码高亮可以强行指定语言,比如 C#,不过其它的,纯文字看着还行,手动选择一下就不对劲了,而且有时候生成的代码是纯粹的 TextBlock 模式,选也没法选

设置代码如下:

<mdxaml:MarkdownScrollViewer
  Name="MarkdownScrollViewer"
  MarkdownStyleName="FluentTheme"
  SelectionEnabled="True">
  <mdxaml:MarkdownScrollViewer.Plugins>
    <mdxaml:MdAvPlugins>
      <avalonia:ChatAISetup />
    </mdxaml:MdAvPlugins>
  </mdxaml:MarkdownScrollViewer.Plugins>
  <mdxaml:MarkdownScrollViewer.Styles>
    <!-- <FluentTheme /> -->
    <!-- <StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" /> -->
    <SimpleTheme />
    <StyleInclude Source="avares://AvaloniaEdit/Themes/Simple/AvaloniaEdit.xaml" />
    <Style Selector="Button.CopyButton TextBlock">
      <Setter Property="Text" Value="📋" />
    </Style>
    <Style Selector=".CodeBlock avedit|TextEditor">
      <Setter Property="ShowLineNumbers" Value="True" />
      <!-- 强行写死 C#,因为目前 MarkdownScrollViewer 有问题,使用 FluentTheme 代码高亮是有了,但是内容会丢失,而且闪烁 -->
      <Setter Property="SyntaxHighlighting" Value="C#" />
    </Style>
  </mdxaml:MarkdownScrollViewer.Styles>
</mdxaml:MarkdownScrollViewer>

太晚了,最后无奈先用着这一版,后面再看吧。

# 2024 年 10 月 4 日

昨天晚上也是,睡不着觉,今天早上 5 点多就醒了,翻来覆去到 8 点,然后开始搞。

本来计划把突然想到的剪切板历史记录给搞出来,而且以为会很快 —— 左右估计不过几个小时,结果调各种效果啥的,愣是又花了一天,今天已经时 4 号了,国庆竟然连 B 站都基本还没打开看过什么....

选择快八点了,刚炒了菜准备吃饭,于是想先记录一下。

最后效果这样:

虽然也搞了些其它东西,比如又重构了下 SK 发送给 LLM 的方式。从下午开始搞的,但是搞这么久也真的是....

不行啊,国庆都连肝 4 天了,这可是一年才有一次的大假期!!!今天还计划把角色、工具人的设置及界面搞了,结果也没时间弄、还有优化流式显示,想着先纯文本流式打字机显示,完了再统一渲染成 Markdown,也是没时间搞了.... 我不想国庆都费再这.... 之前说的 UE5 教程也还没看...

# 2024 年 10 月 5 日

昨天晚上就发现 Avalonia.Markdown 渲染又出问题了,今天早上试了下,存在它的页面切换都要挂。

最后调到中午,发现还是绑出来的问题:

  • 我创建了一个 SimpleMarkdownViewer 封装这一系列设置,SimpleMarkdownViewer 本身挂了一些绑定属性:
public static readonly StyledProperty<string> MarkdownTextProperty =
    AvaloniaProperty.Register<SimpleMarkdownViewer, string>(nameof(MarkdownText));
public static readonly StyledProperty<bool> IsPlaintextProperty =
    AvaloniaProperty.Register<SimpleMarkdownViewer, bool>(nameof(IsPlaintext));
public string MarkdownText
{
    get => GetValue(MarkdownTextProperty);
    set
    {
        // Log.Debug(_stopwatch.ElapsedMilliseconds + "       Setting Markdown to: ");
        SetValue(MarkdownTextProperty, value);
        // _stopwatch.Restart();
    }
}
public string HtmlText
{
    get => MarkdownUtils.ToHtml(MarkdownText);
}
public bool IsPlaintext
{
    get => GetValue(IsPlaintextProperty);
    set => SetValue(IsPlaintextProperty, value);
}

在其它地方使用时,会直接绑定这些属性,然后在它的页面中,本身的 MarkdownScrollViewer 也会绑定它:

<!-- Avalonia.Markdown 渲染显示 -->
<avalonia:MarkdownScrollViewer
  IsVisible="{Binding !IsPlaintext, RelativeSource={RelativeSource AncestorType={x:Type common:SimpleMarkdownViewer}}}"
  SelectionEnabled="True"
  Markdown="{Binding MarkdownText, RelativeSource={RelativeSource AncestorType={x:Type common:SimpleMarkdownViewer}}}">
  <avalonia:MarkdownScrollViewer.Plugins>
    <avalonia:MdAvPlugins>
      <avalonia:ChatAISetup />
    </avalonia:MdAvPlugins>
  </avalonia:MarkdownScrollViewer.Plugins>
  <avalonia:MarkdownScrollViewer.Styles>
    <FluentTheme />
    <StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" />
    <!-- <SimpleTheme /> -->
    <!-- <StyleInclude Source="avares://AvaloniaEdit/Themes/Simple/AvaloniaEdit.xaml" /> -->
    <Style Selector="Button.CopyButton TextBlock">
      <Setter Property="Text" Value="📋" />
    </Style>
    <Style Selector=".CodeBlock avedit|TextEditor">
      <Setter Property="ShowLineNumbers" Value="True" />
      <!-- 强行写死 C#,因为目前 MarkdownScrollViewer 有问题,使用 FluentTheme 代码高亮是有了,但是内容会丢失,而且闪烁 -->
      <!-- <Setter Property="SyntaxHighlighting" Value="C#" /> -->
    </Style>
  </avalonia:MarkdownScrollViewer.Styles>
</avalonia:MarkdownScrollViewer>

结果不知道是不是这样中转导致渲染引擎出问题了,反正就是不报错,单纯就是切换页面的时候,整个页面卸载不掉,然后崩溃。

果然还是太好高骛远了,想着用方便的高级特性,这下栽跟头了.... 白费时间,还不如用最常规、简单的方法:代码中转复制,什么事都没了。毕竟通常事情都是有代价的。


自定义控件定义的组件,其绑定属性跟随的是 DataContext 。

另外,自定义的绑定数据,Set 方法是不会被调用的!!!

public static readonly StyledProperty<bool?> IsPlaintextProperty =
    AvaloniaProperty.Register<SimpleMarkdownViewer, bool?>(nameof(IsPlaintext),
                                                           defaultBindingMode: BindingMode.TwoWay);
public bool? IsPlaintext
{
    get => GetValue(IsPlaintextProperty);
    set
    {
        SetValue(IsPlaintextProperty, value);
        MarkdownScrollViewer.IsVisible = !IsPlaintext ?? false;
        BlockTextScrollViewer.IsVisible = IsPlaintext ?? true;
    }
}

如果想要在 Set 的时候做点什么,必须通过重载来额外判断:

protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
    base.OnPropertyChanged(change);
    if (change.Property == IsPlaintextProperty)
    {
        // 在这里处理 IsPlaintext 属性的变化
        var newValue = change.NewValue.GetValueOrDefault<bool>();
        MarkdownScrollViewer.IsVisible = !newValue;
        BlockTextScrollViewer.IsVisible = newValue;
    }
}

鉴于上面使用 Avalonia.Markdown 的一些问题 (最主要还是配合 Fluent 主题渲染直接报错、配合 Simple 主题也显示难看),而且 Demo 源码也跑不了。

所以折腾半晌,就又把 Avalonia.HtmlRenderer 拿来试了下,效果有,但是不大好,感觉画风都不是一套的了.... 而且真的卡,放几个对话记录就感觉有点吃不消了 —— 也许可以考虑统一封装到 SimpleMarkdownViewer,由设置项控制用哪个渲染。


搞 Avalonia.HtmlRenderer 又折腾到晚上了... 这东西默认会给文本上下加上 20 像素的空白长度..... 字体也不对劲.... 另外排版上最后觉得还是做分页吧,现在这种 Scroll 套 Scroll 进度条变来变去,体验也不好:

今天最后决定还是用 Avalonia.HtmlRenderer 了,因为跟着追源码,一边追一边调,已经发现正确修改这些问题 (包括字体、边距、自适应宽、高) 的方法了。

  • 字体:可以生效,只是里面通过系统文字名字映射的,得完全一致,很不稳定对不上就花了,所以决定自定义字体,全都用一套。
  • 边距:可以考虑 Markdown 渲染成 Html 的时候在就加上 markgin 上下 20px,当然,仅限于第一个

    标签和最后一个。

  • 自适应宽高:重载容器 (SimpleMarkdownViewer) MeasureOverride 方法,对 HtmlLabel 做 AutoSize 下的 MaxWidth 限制。
  • 文字色根据主题切换:渲染 Html 时加上对于主题颜色,OnThemeChanged 事件中重新渲染 Html。

至于 Avalonia.Markdown,代码高亮强依赖 AvaloniaEdit,AvaloniaEdit 强依赖 FluntTheme,而 Avalonia.Markdown 不能使用 FluntTheme,渲染要直接报错.... 只能用 Avalonia.Markdown.FluntTheme 配合 SimpleTheme 主题才能正常渲染,但是代码高亮啥的都炸了,连文字选中表现都不对劲, 一环套一环。

而且源码 Demo 拉下来一堆报错,跑不了。目前就是是之前折腾了一天多的结论,看看维护时间好像也已经没维护了,上面说的渲染报错问题,git issue 有人 push commit 好像也没看到反应,只能先放弃。

结果一通折腾下来,又是 11 点多,国庆还是太肝了... 明天先把字体问题解决了,然后计划:

  • 去掉聊天页面的动态虚拟化 (体验下来,对于存在不固定 Item 的情况下,简直是负优化)
  • 把分页做了
  • 加点工具设置的东西、优化下设置排版
  • 优化下日志排版,现在也是虚拟化, Scroll 体验不对劲,要么改成 Item 固定高度,要么用内联文字?

# 2024 年 10 月 6 日

早上起床就开始找开源字体,准备将应用字体与 Avalonia.HtmlRenderer 保持一致。

  • 注:看代码 HtmlRenderer 只支持一个字体,因此在 html style 定义多个字体的情况不行,找到第一个字体存在就直接用了,不会检测文字是否适合

字体的坑:

  • Avalonia 的字体加载,是依靠字体内部的 FamilyName,一般都看不到....
  • 所以文件路径是不行的,注意连空格都得有,直接打开字体看到的中文名字是不行的!

Avalonia.HtmlRenderer 好像也不行啊,中英文混排巨坑:

搞半天发现这个又坏了... 还是是因为我自己编译导致的?因为现在 nuget 上的版本很旧,自定义字体貌似都不支持,所以才拉下来自己编译了个....


测试了下还真是... 看来最新版本还没发布也是有一定原因的... 看记录今年都有不少修改,但是 nuget 已经是去年的版本了。


反编译查了下 nuget 版本,找到添加字体的入口了,在 HtmlRender 里面,直接是静态方法了,之前还以为可能得反射去查了呢 —— 这不是比改版后的方便么?AvaloniaAdapter nuget 版本是个单例,而且改版后还变成成员了。这样不是还得看啥时候添加、或者每次创建要给都得单独加一次.... 虽然是字典没关系。

反正既然老版本有设置字体的地方,那就直接换回 nuget 版本了,直接在 SimpleMarkdownViewer 静态构造函数里面初始化一次。

我就说今天又忙活了一天,之前好像也没感觉,怎么排版就挂了?差点吓惨了,因为立刻在官方 Demo 上试了下中英文混排也是乱的,都在想是不是要把 Avalonia.Markdown 也加进来,然后整个设置可选项了....

换回 nuget 版本果然没渲染问题,字体看着也 OK:

# 2024 年 10 月 7 日

添加 OpenAI 模式 BPE 分词器计算。 解决聊天 UI 抖动问题,见 使用Avalonia的踩坑记录:ItemsControl,晚上还单独写了篇这个研究记录,导致本篇日志也没时间弄了。

# 总结

记录有点乱,像流水账... 之所以没当天就写完是因为本来想整理下的,但是如今过了两周发现又不知道写啥好,就先这样吧。

毕竟考虑到确实折腾了这么久,不记录一下就没有意义了:没想到国庆肝了七天,抠细节想搞好的话,什么东西都难搞... 本来想国庆研究 UE5 的,结果全搞这个 Avalonia 和 AI 去了。后面上班了甚至感觉好像连续一个月都在上班一样,有点顶不住,接下来准备先缓一缓了。