From 1bba3611934b59fae3f53840512f08878cb41018 Mon Sep 17 00:00:00 2001 From: LIlGG <1103069291@qq.com> Date: Wed, 17 Jun 2026 16:33:21 +0800 Subject: [PATCH] feat: render Live2D chat responses as Markdown --- packages/live2d/package.json | 2 + packages/live2d/src/components/Live2dTips.tsx | 12 ++- .../__tests__/createStreamMessage.test.ts | 27 ++++++ .../helpers/__tests__/renderMarkdown.test.ts | 85 +++++++++++++++++++ packages/live2d/src/helpers/renderMarkdown.ts | 59 +++++++++++++ packages/live2d/src/styles/unocss.global.css | 77 +++++++++++++++++ pnpm-lock.yaml | 71 ++++++++++++++++ .../halo/live2d/agent/AgentToolService.java | 12 ++- .../live2d/agent/AgentToolServiceTest.java | 18 ++++ 9 files changed, 360 insertions(+), 3 deletions(-) create mode 100644 packages/live2d/src/helpers/__tests__/createStreamMessage.test.ts create mode 100644 packages/live2d/src/helpers/__tests__/renderMarkdown.test.ts create mode 100644 packages/live2d/src/helpers/renderMarkdown.ts diff --git a/packages/live2d/package.json b/packages/live2d/package.json index 3be2bc1..5c39e65 100644 --- a/packages/live2d/package.json +++ b/packages/live2d/package.json @@ -27,6 +27,7 @@ "@pixi/sound": "^6.0.1", "iconify-icon": "^3.0.2", "lit": "^3.3.3", + "markdown-it": "^14.2.0", "pixi.js": "^8.13.1", "query-string": "^9.3.1", "react": "^19.2.6", @@ -34,6 +35,7 @@ "untitled-pixi-live2d-engine": "^1.1.0" }, "devDependencies": { + "@types/markdown-it": "^14.1.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@unocss/postcss": "^66.6.8", diff --git a/packages/live2d/src/components/Live2dTips.tsx b/packages/live2d/src/components/Live2dTips.tsx index 6cddefe..b012695 100644 --- a/packages/live2d/src/components/Live2dTips.tsx +++ b/packages/live2d/src/components/Live2dTips.tsx @@ -10,6 +10,10 @@ import type { StreamMessageStartEvent, StreamMessageStopEvent, } from "@/live2d/events/stream-message"; +import { + hasMarkdownBlockElements, + renderMarkdown, +} from "@/live2d/helpers/renderMarkdown"; import { isNotEmpty } from "@/live2d/utils/isNotEmpty"; import { randomSelection } from "@/live2d/utils/randomSelection"; import { consume } from "@lit/context"; @@ -60,6 +64,11 @@ export class Live2dTips extends UnoLitElement { } render(): TemplateResult { + const renderedMessage = this.isStreamMode + ? renderMarkdown(this._message) + : this._message; + const isMarkdownBlock = + this.isStreamMode && hasMarkdownBlockElements(renderedMessage); const classes = { "animate-shake": true, "animate-delay-5s": true, @@ -78,13 +87,14 @@ export class Live2dTips extends UnoLitElement { "text-ellipsis": true, "transition-opacity-1000": true, "break-all": true, + "live2d-tips-markdown": isMarkdownBlock, "opacity-100": this._isShow, "opacity-0": !this._isShow, "select-none": true, }; return html`
- ${unsafeHTML(this._message)} + ${unsafeHTML(renderedMessage)}
`; } diff --git a/packages/live2d/src/helpers/__tests__/createStreamMessage.test.ts b/packages/live2d/src/helpers/__tests__/createStreamMessage.test.ts new file mode 100644 index 0000000..5f04106 --- /dev/null +++ b/packages/live2d/src/helpers/__tests__/createStreamMessage.test.ts @@ -0,0 +1,27 @@ +import { + STREAM_MESSAGE_START_EVENT_NAME, + type StreamMessageStartEventDetail, +} from "@/live2d/events/stream-message"; +import { describe, expect, it, vi } from "vitest"; +import { createStreamMessage } from "../createStreamMessage"; + +describe("createStreamMessage", () => { + it("dispatches stream start with the inactivity timeout", () => { + const listener = vi.fn((event: Event) => { + const detail = (event as CustomEvent) + .detail; + expect(detail).toEqual({ + timeout: 1000, + }); + }); + window.addEventListener(STREAM_MESSAGE_START_EVENT_NAME, listener); + + try { + createStreamMessage(1000, 2000); + } finally { + window.removeEventListener(STREAM_MESSAGE_START_EVENT_NAME, listener); + } + + expect(listener).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/live2d/src/helpers/__tests__/renderMarkdown.test.ts b/packages/live2d/src/helpers/__tests__/renderMarkdown.test.ts new file mode 100644 index 0000000..3681a69 --- /dev/null +++ b/packages/live2d/src/helpers/__tests__/renderMarkdown.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, it } from "vitest"; +import { hasMarkdownBlockElements, renderMarkdown } from "../renderMarkdown"; + +describe("renderMarkdown", () => { + it("renders common markdown blocks and inline marks", () => { + const html = renderMarkdown( + [ + "## 标题", + "", + "这里有 **重点**、`code` 和 [链接](https://example.com?a=1&b=2)。", + "", + "- 第一项", + "- 第二项", + ].join("\n"), + ); + + expect(html).toContain("

标题

"); + expect(html).toContain("重点"); + expect(html).toContain("code"); + expect(html).toContain( + '链接', + ); + expect(html).toContain("