mirror of
https://github.com/JOYCEQL/magic-resume.git
synced 2026-06-01 23:38:48 +02:00
feat: Introduce Markdown processing for the AI proofreading function, use Streamdown for rendering,add Alibaba Bold font
This commit is contained in:
@@ -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",
|
||||
|
||||
Generated
+1038
-4
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user