Joey

我是如何创建TMDX的

这篇文章会介绍我如何产生做tmdx的想法,以及我是如何实现这个想法的

我是如何创建TMDX的

起源

我很久以前就开始写博客了,最开始用的是jekyll搭建,后来使用 gatsby,但是这些静态站点生成器用起来都很麻烦,需要配置git,需要学习如何构建,发图片很麻烦,需要手动复制图片到图片文件夹。 于是我又切换到了medium,但medium的写作体验一直不太好,不支持markdown,不支持代码高亮,而且界面也不够简洁。我一直在寻找一个更好的写作平台。

7月份的时候我做了一个博客打赏的插件叫 TonTip 它的作用是帮助个人站长获得区块链打赏收入,但是我推广了一圈发现反响不大,所以我下定决心要自己搭建一个更符合我需求的博客平台,能集成任何我想要集成的插件。

开始

网上有很多个人博客的搭建教程,我看了一些,然后有了基本的技术思路,我决定使用:

  1. svelte + sveltekit
  2. bun + typescript
  3. cloudflare
  4. tailwindcss
  5. daisyui
  6. shiki
  7. markdown-it
  8. monaco editor
  9. mathjax

选择 svelte 是因为这个框架能将模板代码编译为html和js,并且消除不必要的运行时依赖,选择bun是因为这是一个最近很火的js运行时,tailwindcss是一个很简单的原子框架,使用它新手也能快速搭建漂亮的界面。daisyui是一个tailwindcss ui组件库,使用它很多基本的组件都不用自己写。markdown-it是用来编译markdown的,还可以选择remarkjs,但是我决定使用markdown-it,因为它更加简洁。shiki是一个代码语法高亮的库,使用它可以实现在markdown中插入漂亮的代码。mathjax是一个数学库,如果需要编辑latex公式,需要依赖这个库。最后也是最重要的编辑器部分是基于monaco来做的,这是微软团队开源的一个能嵌入到浏览器内的编辑器,扩展性也很强。

产品设计

产品设计的思路比较简单,我参考了很多人的博客,来设计我的博客界面。至于编辑器部分,则完全参考了github copilot,我是github copilot的订阅用户,这个插件能帮忙补齐高质量的代码,对于程序员来说是非常棒的生产力提升工具。我希望我的编辑器也能像GitHub Copilot那样,提供智能的代码补全和建议,这样不仅能提高写作效率,还能让内容更加专业和准确。

这是vscode + github copilot 的编辑界面

image.png

这是 tmdx 的编辑界面

image.png

整体功能和界面设计都是接近的

图片库实现

图片库对于编辑体验来说很重要,我们不希望在编辑过程中,还打开本地文件夹寻找图片,然后上传。因此我内置了一个图片上传和管理功能。用户可以直接在图片库中上传图片,图片会自动存储到云端,并在文章中插入图片链接。这样不仅简化了操作流程,还能确保图片的统一管理和快速访问。

这是图片库的界面

image.png

支持鼠标点击选择文件上传,支持Ctrl + V 粘贴上传,以及拖拽文件到图片库触发上传

有时候不希望通过图片库上传图片,我还监听了编辑器的事件,实现了编辑器内粘贴、拖拽上传

editor.getDomNode()?.addEventListener("paste", handlePaste);
editor.getDomNode()?.addEventListener("dragover", handleDragOver);
editor.getDomNode()?.addEventListener("drop", handleDrop);

AI 对话的实现

自从23年chatgpt问世以来,到现在对话机器人已经很多了,接口的费用也已经降到很低了。要实现一个对话界面也很简单,就是在一个容器内交替展示问题和答案,底部通过一个textarea编辑问题发送。因为聊天接口的响应速度比较慢,而且是一个token一个token的返回的,所以在做界面的过程中用到了svelte一个比较方便的特性 rune,使用 rune 进行反应式编程,这样接口每多返回一个token,都能立刻显示在界面上。

下面这个截图展示了我是如何通过反应式编程,以及http接口的SSE特性,来更新 messages 然后更新界面的

image.png

我最早只集成了 deepseek 一个模型,后来又陆续体验了一些其它厂商的模型,如chatglm,发现也不错,所以也加入进来了,现在支持的模型有:

  1. deepseek:只支持文本对话
  2. glm:只支持文本对话
  3. flux:只支持文本生成图片
  4. cogview:文本生成图片
  5. cogvideox:文本生成视频

对于文生图这点,我个人喜欢拿来生成博客的封面图,这样我就可以轻松生成高质量的封面图,而不需要手动设计或寻找图片。

比如我让flux生成如下提示词的图片:

"blue sky, white cloud"

它返回了下面这张图,

blue sky, white cloud

我只要复制链接,设置为 frontmatter 的 cover 字段就行

AI 补齐的实现

AI补齐是一个比较有技术门槛的事情,我现在还能想起2019年,tabnine刚出现时,大家对它的称赞和震惊。然后有了github copilot,不过那会儿 llm 技术还不发达,copilot的补齐也不太准确,直到2023年,chatgpt出现后,人们发现可以通过大模型来实现代码补齐,于是微软迅速更新了github copilot,并且一直迭代直到现在补齐效果已经是非常棒了。我决定在我的编辑器中也集成AI补齐的功能,这样用户在写作时可以享受到类似GitHub Copilot的智能补全体验。为了实现这一功能,我选择了 deepseek 模型作为后端支持,因为它在自然语言处理和代码补全方面表现出色。通过API调用,编辑器可以实时获取补全建议,并将其显示在编辑器中,用户只需按下Tab键即可接受建议。

monaco editor 提供了补齐的接口,我们只要实现这个接口就能出现想要的补齐内容了

monaco.languages.registerInlineCompletionsProvider(
    "markdown",
    {
        provideInlineCompletions: async (
            model,
            position,
            context,
            token,
        ) => {
            // generate completions...
        }
    }
);

如何快速,高效,准确地生成补齐内容是核心难点,为此我参考了很多人的实现,包括

  1. https://spencerporter2.medium.com/building-copilot-on-the-web-f090ceb9b20b
  2. https://github.com/arshad-yaseen/monacopilot
  3. https://sourcegraph.com/blog/the-lifecycle-of-a-code-ai-completion

目前我使用的是最简单的方式,定时发送补齐请求给AI,然后缓存内容,在需要的时候展示给用户。

我有一个表记录了补齐耗时,下图是部分记录的截图

image.png

第三列是生成的补齐内容的长度,第四列是接口耗时,基本能在5s内返回,对于内容创作者来说还是能接受的,未来也还有优化空间。

我没有去特意统计补齐内容的质量,用户接受情况,我在编辑这篇文章的过程中,截图了一些补齐的结果,个人感觉是不错的

image.png

image.png

image.png

image.png

AI 搜索的实现

我在 Navbar 处放置了一个搜索框

image.png

不像传统的搜索使用 elasticsearch 或者 algolia 这些,我基于cloud flare AI 提供的 vectorize 向量数据库,实现了一个 AI 搜索功能,每次用户发布文章,会触发AI去分析文章,生成 embeddings,然后保存到向量数据库中,用户在进行搜索时,是通过比对向量相似度进行的,而不是传统的关键词匹配。这种方式能够更准确地理解用户的搜索意图,提供更相关的搜索结果。

AI 翻译的实现

我不是英语母语者,而且我的英语也不太好,但是我希望我的文章能被世界上所有人看到并且理解,所以我决定在我的博客平台中集成AI翻译功能。这样,用户在发布文章时,可以选择将文章翻译成多种语言,从而扩大文章的受众范围。为了实现这一功能,我最初是考虑使用google translate 来翻译的,不过使用后发现它不太能理解上下文的意思,翻译结果很生硬,后来我又测试了一些 AI 翻译的接口,最后决定使用 deepseek 来实现翻译

const systemPrompt = `你是一个 AI 翻译助手,请将用户输入的markdown文件翻译为<lang>${lang}</lang>,

要求:
1. 跳过markdown里的图片,链接,引用,代码
2. 如果有frontmatter,只翻译里面的title, description字段
3. 如果无法翻译到目标语言,则保持原文
`;
const output = await aiClient.chat.completions.create({
    model: 'deepseek-chat',
    messages: [
        { "role": "system", "content": systemPrompt },
        { "role": "user", "content": mdContent }
    ],
    stream: false,
});

哈哈,是不是很简单?

使用翻译也很简单,在文章的顶部有个语言选择栏,选中你希望阅读的语言就行

image.png

总结

我目前一个人在 TMDX 上已经花了2个月的时间了,但是功能还比较简陋,未来还需要不断迭代,大家觉得我这个点子如何?

preview

Discussion (0)