feat: Introduce Markdown processing for the AI proofreading function, use Streamdown for rendering,add Alibaba Bold font

This commit is contained in:
JOYCEQL
2026-02-27 13:43:46 +08:00
parent 1884bd851e
commit 2bc2664593
7 changed files with 1103 additions and 30 deletions
+4
View File
@@ -59,6 +59,7 @@
"lodash": "^4.17.21",
"lucide-react": "^0.379.0",
"mark.js": "^8.11.1",
"markdown-exit": "1.0.0-beta.8",
"next-themes": "^0.4.3",
"puppeteer": "^23.9.0",
"puppeteer-core": "^23.9.0",
@@ -69,8 +70,10 @@
"react-resizable-panels": "^2.0.20",
"sharp": "^0.33.5",
"sonner": "^1.7.1",
"streamdown": "^2.3.0",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"turndown": "^7.2.2",
"uuid": "^11.0.5",
"vaul": "^1.1.1",
"zustand": "^4.5.4"
@@ -80,6 +83,7 @@
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/turndown": "^5.0.6",
"@vitejs/plugin-react": "^5.1.4",
"eslint": "^8",
"postcss": "^8",
+1038 -4
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
+8
View File
@@ -5,3 +5,11 @@
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Alibaba PuHuiTi";
src: url("/fonts/AlibabaPuHuiTi-3-85-Bold.ttf") format("truetype");
font-weight: bold;
font-style: normal;
font-display: swap;
}
+48 -22
View File
@@ -2,6 +2,10 @@ import { useEffect, useState, useRef } from "react";
import { Loader2, Sparkles } from "lucide-react";
import { toast } from "sonner";
import { useTranslations } from "@/i18n/compat/client";
import { Streamdown } from "streamdown";
import "streamdown/styles.css";
import { createMarkdownExit } from "markdown-exit";
import TurndownService from "turndown";
import {
Dialog,
@@ -23,6 +27,19 @@ interface AIPolishDialogProps {
onApply: (content: string) => void;
}
// markdown-exit 实例,用于将 AI 返回的 Markdown 转换为 Tiptap 兼容的 HTML
const md = createMarkdownExit({
html: true, // 允许 HTML 标签透传
breaks: true, // 将换行符转换为 <br>
linkify: false, // 简历内容不需要自动识别链接
});
// turndown 实例,用于将 Tiptap HTML 转换为 Markdown 发给 AI
const turndownService = new TurndownService({
headingStyle: "atx",
bulletListMarker: "-",
});
export default function AIPolishDialog({
open,
onOpenChange,
@@ -66,14 +83,14 @@ export default function AIPolishDialog({
"Content-Type": "application/json"
},
body: JSON.stringify({
content,
content: turndownService.turndown(content),
apiKey: selectedModel === "doubao" ? doubaoApiKey : selectedModel === "openai" ? openaiApiKey : deepseekApiKey,
apiEndpoint: selectedModel === "openai" ? openaiApiEndpoint : undefined,
model:
selectedModel === "doubao"
? doubaoModelId
: selectedModel === "openai" ? openaiModelId
: config.requiresModelId ? deepseekModelId : deepseekApiKey,
selectedModel === "doubao"
? doubaoModelId
: selectedModel === "openai" ? openaiModelId
: config.requiresModelId ? deepseekModelId : deepseekApiKey,
modelType: selectedModel
}),
signal: abortControllerRef.current.signal
@@ -95,16 +112,7 @@ export default function AIPolishDialog({
if (done) break;
const chunk = decoder.decode(value);
setPolishedContent((prev) => {
const newContent = prev + chunk;
requestAnimationFrame(() => {
if (polishedContentRef.current) {
const container = polishedContentRef.current;
container.scrollTop = container.scrollHeight;
}
});
return newContent;
});
setPolishedContent((prev) => prev + chunk);
}
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
@@ -119,6 +127,16 @@ export default function AIPolishDialog({
}
};
// 自动滚动到底部
useEffect(() => {
if (polishedContent && polishedContentRef.current) {
const container = polishedContentRef.current;
requestAnimationFrame(() => {
container.scrollTop = container.scrollHeight;
});
}
}, [polishedContent]);
useEffect(() => {
if (open) {
handlePolish();
@@ -141,7 +159,11 @@ export default function AIPolishDialog({
};
const handleApply = () => {
onApply(polishedContent);
// 将 Markdown 转为 HTML,并补回 Tiptap 所需的 ul/ol 类名
const htmlContent = md.render(polishedContent)
.replace(/<ul>/g, '<ul class="custom-list">')
.replace(/<ol>/g, '<ol class="custom-list-ordered">');
onApply(htmlContent);
handleClose();
toast.success(t("error.applied"));
};
@@ -224,13 +246,14 @@ export default function AIPolishDialog({
"p-6 h-[400px] overflow-auto shadow-sm"
)}
>
<div
<Streamdown
className={cn(
"prose dark:prose-invert max-w-none",
"text-neutral-700 dark:text-neutral-300"
)}
dangerouslySetInnerHTML={{ __html: content }}
/>
>
{turndownService.turndown(content)}
</Streamdown>
</div>
</div>
@@ -260,13 +283,16 @@ export default function AIPolishDialog({
"p-6 h-[400px] overflow-auto shadow-sm scroll-smooth"
)}
>
<div
<Streamdown
animated
isAnimating={isPolishing}
className={cn(
"prose dark:prose-invert max-w-none",
"text-neutral-800 dark:text-neutral-200"
)}
dangerouslySetInnerHTML={{ __html: polishedContent }}
/>
>
{polishedContent}
</Streamdown>
</div>
</div>
</div>
+3 -3
View File
@@ -28,7 +28,7 @@ export const Route = createFileRoute("/api/polish")({
messages: [
{
role: "system",
content: `你是一个专业的简历优化助手。请帮助优化以下文本,使其更加专业和有吸引力。
content: `你是一个专业的简历优化助手。请帮助优化以下 Markdown 格式的文本,使其更加专业和有吸引力。
优化原则:
1. 使用更专业的词汇和表达方式
@@ -36,9 +36,9 @@ export const Route = createFileRoute("/api/polish")({
3. 保持简洁清晰
4. 使用主动语气
5. 保持原有信息的完整性
6. 保留我输入的格式
6. 严格保留原有的 Markdown 格式结构(列表项保持为列表项,加粗保持加粗等)
请直接返回优化后的文本,不要包含任何解释或其他内容。`
请直接返回优化后的 Markdown 文本,不要包含任何解释或其他内容。`
},
{
role: "user",
+2 -1
View File
@@ -9,7 +9,8 @@ const config = {
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
"./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}"
"./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}",
"./node_modules/streamdown/dist/*.js"
],
prefix: "",
theme: {