Electron Forge【实战】桌面应用 —— AI聊天(下)

此为系列教程,需先完成

会话列表按更新时间倒序加载

src/db.ts

ts 复制代码
db.version(1).stores({
  // 主键为id,且自增
  // 新增updatedAt字段,用于排序
  conversations: "++id, updatedAt",
});

src/stores/conversation.ts

ts 复制代码
    // 从本地存储中查询出会话列表
    async fetchConversations() {
      const items = await db.conversations.orderBy('updatedAt') // 按更新日期排序
        .reverse() // 倒序排列
        .toArray(); // 转换为数组
      this.items = items;
    },

新创建的会话,在会话列表顶部

src/stores/conversation

ts 复制代码
      //   pinia 中新增会话
      this.items.unshift({
        id: newCId,
        ...createdData,
      });

会话更新时,同步存储到本地

src/views/Conversation.vue

ts 复制代码
      // 本次回答结束后
      if (data.is_end) {
        // 清空流式消息的内容
        streamContent = "";

        // 更新会话时,需要移除 id 字段,否则会报错
        let temp_convsersation = JSON.parse(
          JSON.stringify(convsersation.value)
        );
        delete temp_convsersation.id;

        await conversationStore.updateConversation(
          convsersation.value!.id,
          temp_convsersation
        );
      }

src/stores/conversation.ts

ts 复制代码
    async updateConversation(id: number, newData: Omit<ConversationProps, "id">) {
      // 本地存储中更新会话
      await db.conversations.update(id, newData);
      //   pinia 中更新会话
      const index = this.items.findIndex((item) => item.id === id);
      if (index > -1) {
        this.items[index] = { id, ...newData };
      }
    },

聊天区自动滚动到底部

src/components/MessageList.vue

需对外暴露 ref

html 复制代码
<div class="message-list" ref="_ref">
ts 复制代码
const _ref = ref<HTMLDivElement>();

defineExpose({
  ref: _ref,
});

src/views/Conversation.vue

html 复制代码
<MessageList :messages="convsersation!.msgList" ref="messageListRef" />
ts 复制代码
const messageListRef = ref<{ ref: HTMLDivElement }>();

const messageScrollToBottom = async (behavior?: string) => {
  await nextTick();
  if (messageListRef.value) {
    // 获取到自定义组件内的真实 ref 调用 scrollIntoView 
    messageListRef.value.ref.scrollIntoView({
      block: "end",
      behavior: behavior as ScrollBehavior, // "auto" | "instant" | "smooth"
    });
  }
};

会话页初次加载时

在 onMounted 末尾添加

ts 复制代码
await messageScrollToBottom();

切换当前会话时

ts 复制代码
watch(
  () => route.params.id,
  async (newId: string) => {
    conversationId.value = parseInt(newId);
    // 切换当前会话时,聊天区自动滚动到底部
    await messageScrollToBottom();
  }
);

AI 流式回答问题时

onUpdateMessage 内

ts 复制代码
      // 根据消息id, 获取到 loading 状态的消息
      let msg = convsersation.value!.msgList[messageId];
      // 将 AI 回答的流式消息替换掉 loading 状态的消息
      msg.content = streamContent;
      // 根据 AI 的返回,更新消息的状态
      msg.status = getMessageStatus(data);
      // 用 dayjs 得到格式化的当前时间字符串
      msg.updatedAt = dayjs().format("YYYY-MM-DD HH:mm:ss");

      // 顺滑滚动到底部
      await messageScrollToBottom("smooth");

滚动性能优化 -- 仅当 AI 回答超过一行时才触发滚动

ts 复制代码
let currentMessageListHeight = 0;
const checkAndScrollToBottom = async () => {
  if (messageListRef.value) {
    const newHeight = messageListRef.value.ref.clientHeight;
    if (newHeight > currentMessageListHeight) {
      currentMessageListHeight = newHeight;
      await messageScrollToBottom("smooth");
    }
  }
};

onUpdateMessage 内改为

ts 复制代码
      // 顺滑滚动到底部
      await nextTick()
      await checkAndScrollToBottom()

切换会话时记得重置 currentMessageListHeight

ts 复制代码
watch(
  () => route.params.id,
  async (newId: string) => {
    conversationId.value = parseInt(newId);
    // 切换当前会话时,聊天区自动滚动到底部
    await messageScrollToBottom();
    // 切换当前会话时,将当前会话的消息列表的高度重置为0
    currentMessageListHeight = 0;
  }
);

恢复默认样式

Tailwind CSS 默认移除了几乎所有的默认样式,导致无法渲染带格式的富文本,通过插件 @tailwindcss/typography 来重置一套比较合理的默认样式

ts 复制代码
npm install -D @tailwindcss/typography

src/index.css 中添加

css 复制代码
@plugin "@tailwindcss/typography";

给目标内容加上类名 prose 即可

html 复制代码
<div class="prose">要恢复样式的内容</div>

自定义默认样式

css 复制代码
// 给 p 标签添加 my-1 的 Tailwind CSS 样式
prose-p:my-1

更多自定义样式的方法见
https://github.com/tailwindlabs/tailwindcss-typography?tab=readme-ov-file#element-modifiers

渲染 markdown 的内容(支持代码高亮)

AI 模型返回的都是 markdown 语法的文本,需要按 markdown 进行格式化渲染

ts 复制代码
npm install vue-markdown-render markdown-it-highlightjs --save

src/components/MessageList.vue 中使用

ts 复制代码
import VueMarkdown from "vue-markdown-render";
import markdownItHighlightjs from "markdown-it-highlightjs";

const plugins = [markdownItHighlightjs];

要渲染的内容,改用 vue-markdown 组件,通过 source 传入

html 复制代码
            <div
              v-else
              class="prose prose-slate prose-hr:my-0 prose-li:my-0 prose-ul:my-3 prose-p:my-1 prose-pre:p-0"
            >
              <vue-markdown :source="message.content" :plugins="plugins" />
            </div>
相关推荐
caiyueloveclamp10 小时前
【功能介绍05】ChatPPT好不好用?如何用?用户操作手册来啦!——【AI辅写+分享篇】
人工智能·powerpoint·ai生成ppt·aippt·免费aippt
Aileen_0v010 小时前
【Gemini3.0的国内use教程】
android·人工智能·算法·开源·mariadb
xiaogutou112110 小时前
5款软件,让歌唱比赛海报设计更简单
人工智能
后端小张10 小时前
智眼法盾:基于Rokid AR眼镜的合同条款智能审查系统开发全解析
人工智能·目标检测·计算机视觉·ai·语言模型·ar·硬件架构
dalalajjl10 小时前
每个Python开发者都应该试试知道创宇AiPy!工作效率提升500%的秘密武器
大数据·人工智能
wheeldown10 小时前
【Rokid+CXR-M】基于Rokid CXR-M SDK的博物馆AR导览系统开发全解析
c++·人工智能·ar
爱看科技10 小时前
AI智能计算竞赛“战火重燃”,谷歌/高通/微美全息构建AI全栈算力开启巅峰角逐新篇
人工智能
IT_陈寒11 小时前
Redis性能翻倍的5个冷门技巧,90%开发者都不知道第3个!
前端·人工智能·后端
晨非辰11 小时前
C++ 波澜壮阔 40 年:从基础I/O到函数重载与引用的完整构建
运维·c++·人工智能·后端·python·深度学习·c++40周年
鼎道开发者联盟11 小时前
智能原生操作系统畅想:人智共生新时代的基石
人工智能·机器学习·自然语言处理