feat: ai config deepseek

This commit is contained in:
JOYCEQL
2025-01-14 13:18:04 +08:00
committed by qingchen
parent adaafa95dd
commit 8e1b7aba05
11 changed files with 476 additions and 243 deletions
+42 -36
View File
@@ -1,48 +1,54 @@
import { NextRequest, NextResponse } from "next/server";
import { AIModelType } from "@/store/useAIConfigStore";
import { AI_MODEL_CONFIGS } from "@/config/ai";
export async function POST(req: NextRequest) {
try {
const body = await req.json();
const { apiKey, model, content } = body;
const { apiKey, model, content, modelType } = body;
const response = await fetch(
"https://ark.cn-beijing.volces.com/api/v3/chat/completions",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
const modelConfig = AI_MODEL_CONFIGS[modelType as AIModelType];
if (!modelConfig) {
throw new Error("Invalid model type");
}
const response = await fetch(modelConfig.url, {
method: "POST",
headers: modelConfig.headers(apiKey),
body: JSON.stringify({
model: modelConfig.requiresModelId ? model : modelConfig.defaultModel,
response_format: {
type: "json_object",
},
body: JSON.stringify({
model,
messages: [
{
role: "system",
content: `你是一个专业的中文简历语法检查助手。请完整检查以下文本中的语法语境错别字,包括:
1. 只考虑语法组词中的错别字
messages: [
{
role: "system",
content: `你是一个专业的中文简历错别字检查助手。请帮助检查以下文本,以下是要求:
1.仅需考虑组词中的错别字,不要考虑语法和格式错误。
2.对每个发现的问题,请按JSON格式返回
对每个发现的问题,请以JSON格式返回,格式如下
{
"errors": [
{
"text": "错误的文本",
"message": "详细的错误说明",
"type": "spelling"或"grammar",
"suggestions": ["建议修改1", "建议修改2"]
}
]
}
以下是示例格式
{
"errors": [
{
"text": "错误的文本",
"message": "详细的错误说明",
"type": "spelling"或"grammar",
"suggestions": ["建议修改1", "建议修改2"]
}
]
}
请确保返回的是可解析的JSON格式。`,
},
{
role: "user",
content: content,
},
],
}),
}
);
请确保返回的格式可以正常解析`,
},
{
role: "user",
content: content,
},
],
}),
});
const data = await response.json();
+36 -35
View File
@@ -1,44 +1,45 @@
import { NextResponse } from "next/server";
import { AIModelType } from "@/store/useAIConfigStore";
import { AI_MODEL_CONFIGS } from "@/config/ai";
export async function POST(req: Request) {
try {
const body = await req.json();
const { apiKey, model, content } = body;
const { apiKey, model, content, modelType } = body;
const response = await fetch(
"https://ark.cn-beijing.volces.com/api/v3/chat/completions",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model,
messages: [
{
role: "system",
content: `你是一个专业的简历优化助手。请帮助优化以下文本,使其更加专业和有吸引力。
优化原则:
1. 使用更专业的词汇和表达方式
2. 突出关键成就和技能
3. 保持简洁清晰
4. 使用主动语气
5. 保持原有信息的完整性
6. 保留我输入的格式
请直接返回优化后的文本,不要包含任何解释或其他内容。`,
},
{
role: "user",
content,
},
],
stream: true,
}),
}
);
const modelConfig = AI_MODEL_CONFIGS[modelType as AIModelType];
if (!modelConfig) {
throw new Error("Invalid model type");
}
const response = await fetch(modelConfig.url, {
method: "POST",
headers: modelConfig.headers(apiKey),
body: JSON.stringify({
model: modelConfig.requiresModelId ? model : modelConfig.defaultModel,
messages: [
{
role: "system",
content: `你是一个专业的简历优化助手。请帮助优化以下文本,使其更加专业和有吸引力。
优化原则:
1. 使用更专业的词汇和表达方式
2. 突出关键成就和技能
3. 保持简洁清晰
4. 使用主动语气
5. 保持原有信息的完整性
6. 保留我输入的格式
请直接返回优化后的文本,不要包含任何解释或其他内容。`,
},
{
role: "user",
content,
},
],
stream: true,
}),
});
const encoder = new TextEncoder();
const stream = new ReadableStream({
@@ -15,6 +15,13 @@ import { useAIConfigStore } from "@/store/useAIConfigStore";
import { Separator } from "@/components/ui/separator";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
getFileHandle,
@@ -30,8 +37,18 @@ const SettingsPage = () => {
useState<FileSystemDirectoryHandle | null>(null);
const [folderPath, setFolderPath] = useState<string>("");
const { doubaoApiKey, doubaoModelId, setDoubaoApiKey, setDoubaoModelId } =
useAIConfigStore();
const {
selectedModel,
doubaoApiKey,
doubaoModelId,
deepseekApiKey,
deepseekModelId,
setSelectedModel,
setDoubaoApiKey,
setDoubaoModelId,
setDeepseekApiKey,
setDeepseekModelId,
} = useAIConfigStore();
const t = useTranslations();
@@ -79,160 +96,236 @@ const SettingsPage = () => {
}
};
const handleApiKeyChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const handleApiKeyChange = async (
e: React.ChangeEvent<HTMLInputElement>,
type: "doubao" | "deepseek"
) => {
const newApiKey = e.target.value;
setDoubaoApiKey(newApiKey);
if (type === "doubao") {
setDoubaoApiKey(newApiKey);
} else {
setDeepseekApiKey(newApiKey);
}
};
const handleModelIdChange = async (
e: React.ChangeEvent<HTMLInputElement>
e: React.ChangeEvent<HTMLInputElement>,
type: "doubao" | "deepseek"
) => {
const newModelId = e.target.value;
setDoubaoModelId(newModelId);
if (type === "doubao") {
setDoubaoModelId(newModelId);
} else {
setDeepseekModelId(newModelId);
}
};
return (
<div className="flex-1 space-y-6 px-4 pt-6 mx-auto">
<div className="flex-1 space-y-8 px-4 pt-6 mx-auto max-w-4xl">
<div className="flex items-center justify-between">
<h2 className="text-3xl font-bold tracking-tight bg-gradient-to-r from-indigo-500 to-purple-500 text-transparent bg-clip-text">
<h2 className="text-3xl font-bold tracking-tight bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-transparent bg-clip-text">
{t("dashboard.settings.title")}
</h2>
</div>
<Separator className="my-6" />
<div className="grid gap-6 grid-cols-1 md:grid-cols-2">
<Card
className={cn(
"col-span-1 border-2 transition-all duration-200",
"hover:border-indigo-500/20 hover:shadow-lg"
)}
>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-xl">
<div className="p-2 rounded-lg bg-indigo-50 dark:bg-indigo-950">
<Folder className="h-5 w-5 text-indigo-500" />
<div className="grid gap-8">
{/* 同步目录卡片 */}
<Card className="overflow-hidden border-0 shadow-lg shadow-indigo-500/5 hover:shadow-indigo-500/10 transition-all duration-300">
<CardHeader className="space-y-4 bg-gradient-to-b from-slate-50 to-white dark:from-slate-950 dark:to-slate-900">
<CardTitle className="flex items-center gap-3">
<div className="p-2.5 rounded-xl bg-gradient-to-br from-indigo-500/10 to-purple-500/10 dark:from-indigo-500/20 dark:to-purple-500/20">
<Folder className="h-6 w-6 text-indigo-600 dark:text-indigo-400" />
</div>
{t("dashboard.settings.syncDirectory.title")}
<span className="bg-gradient-to-br from-indigo-600 to-purple-600 dark:from-indigo-400 dark:to-purple-400 text-transparent bg-clip-text">
{t("dashboard.settings.sync.title")}
</span>
</CardTitle>
<CardDescription className="text-sm text-muted-foreground">
{t("dashboard.settings.syncDirectory.description")}
<CardDescription className="text-base text-slate-600 dark:text-slate-400">
{t("dashboard.settings.sync.description")}
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center gap-4">
<div className="flex-1">
{folderPath ? (
<div className="space-y-2">
<Label className="text-sm font-medium">
{t(
"dashboard.settings.syncDirectory.currentSyncFolder"
)}
</Label>
<div className="flex items-center gap-2">
<Badge
variant="secondary"
className="text-xs py-2 px-3"
>
<Folder className="h-3 w-3 mr-1" />
{folderPath}
</Badge>
</div>
</div>
) : (
<div className="text-center py-6 bg-muted/30 rounded-lg">
<p className="text-sm text-muted-foreground">
{t(
"dashboard.settings.syncDirectory.noFolderConfigured"
)}
</p>
</div>
)}
</div>
<Button
variant={folderPath ? "outline" : "default"}
onClick={handleSelectDirectory}
className={cn(
"shrink-0",
!folderPath &&
"text-white hover:from-indigo-600 hover:to-purple-600"
)}
>
{folderPath
? t("dashboard.settings.syncDirectory.changeFolder")
: t("dashboard.settings.syncDirectory.selectFolder")}
</Button>
</div>
<CardContent className="pt-6 space-y-4 px-8 pb-8">
<div className="flex items-center gap-3">
<Input
value={folderPath}
readOnly
className="h-11 bg-slate-50 dark:bg-slate-900 border-slate-200 dark:border-slate-800 focus-visible:ring-indigo-500"
placeholder={t(
"dashboard.settings.syncDirectory.noFolderConfigured"
)}
/>
<Button
onClick={handleSelectDirectory}
className="h-11 min-w-[120px] bg-gradient-to-r from-indigo-500 to-purple-500 hover:from-indigo-600 hover:to-purple-600 text-white shadow-md shadow-indigo-500/20 hover:shadow-indigo-500/30 transition-all duration-300"
>
{t("dashboard.settings.sync.select")}
</Button>
</div>
{directoryHandle && (
<Badge
variant="outline"
className="gap-2 py-1.5 text-sm border-slate-200 dark:border-slate-800"
>
<Folder className="h-4 w-4" />
{folderPath}
</Badge>
)}
</CardContent>
</Card>
<Card
className={cn(
"col-span-1 border-2 transition-all duration-200",
"hover:border-purple-500/20 hover:shadow-lg"
)}
>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-xl">
<div className="p-2 rounded-lg bg-purple-50 dark:bg-purple-950">
<Bot className="h-5 w-5 text-purple-500" />
{/* AI配置卡片 */}
<Card className="overflow-hidden border-0 shadow-lg shadow-indigo-500/5 hover:shadow-indigo-500/10 transition-all duration-300">
<CardHeader className="space-y-4 bg-gradient-to-b from-slate-50 to-white dark:from-slate-950 dark:to-slate-900">
<CardTitle className="flex items-center gap-3">
<div className="p-2.5 rounded-xl bg-gradient-to-br from-indigo-500/10 to-purple-500/10 dark:from-indigo-500/20 dark:to-purple-500/20">
<Bot className="h-6 w-6 text-indigo-600 dark:text-indigo-400" />
</div>
{t("dashboard.settings.aiConfig.title")}
<span className="bg-gradient-to-br from-indigo-600 to-purple-600 dark:from-indigo-400 dark:to-purple-400 text-transparent bg-clip-text">
{t("dashboard.settings.ai.title")}
</span>
</CardTitle>
<CardDescription className="text-sm text-muted-foreground">
{t("dashboard.settings.aiConfig.description")}
<CardDescription className="text-base text-slate-600 dark:text-slate-400">
{t("dashboard.settings.ai.description")}
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div className="space-y-5">
<div className="space-y-3">
<Label className="flex items-center gap-2 text-sm font-medium">
<div className="p-1.5 rounded-md bg-purple-50 dark:bg-purple-950">
<Key className="h-4 w-4 text-purple-500" />
</div>
{t("dashboard.settings.aiConfig.apiKey.label")}
</Label>
<Input
id="doubaoApiKey"
type="password"
value={doubaoApiKey}
onChange={handleApiKeyChange}
placeholder={t(
"dashboard.settings.aiConfig.apiKey.placeholder"
)}
className="font-mono bg-muted/30"
/>
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<span>
{t("dashboard.settings.aiConfig.apiKey.description")}
<CardContent className="pt-6 space-y-8 px-8 pb-8">
<div className="space-y-3">
<Label className="text-base text-slate-700 dark:text-slate-300">
{t("dashboard.settings.ai.selectedModel")}
</Label>
<Select
value={selectedModel}
onValueChange={(value) =>
setSelectedModel(value as "doubao" | "deepseek")
}
>
<SelectTrigger className="h-11 bg-slate-50 dark:bg-slate-900 border-slate-200 dark:border-slate-800">
<SelectValue placeholder="Select a model" />
</SelectTrigger>
<SelectContent>
<SelectItem value="doubao">
<span className="font-medium">
{t("dashboard.settings.ai.doubao.title")}
</span>
<ExternalLink className="h-3 w-3" />
</SelectItem>
<SelectItem value="deepseek">
<span className="font-medium">
{t("dashboard.settings.ai.deepseek.title")}
</span>
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-6">
<div
className={cn(
"space-y-6 rounded-xl p-6 transition-all duration-300",
selectedModel === "doubao"
? "bg-gradient-to-br from-slate-50 to-white dark:from-slate-900 dark:to-slate-900/50 border border-indigo-100 dark:border-indigo-500/20 shadow-lg shadow-indigo-500/5"
: "bg-slate-50/50 dark:bg-slate-900/50 border border-slate-200 dark:border-slate-800/50 opacity-50"
)}
>
<h3 className="text-lg font-semibold flex items-center gap-2 text-slate-800 dark:text-slate-200">
<Bot className="h-5 w-5 text-indigo-600 dark:text-indigo-400" />
{t("dashboard.settings.ai.doubao.title")}
</h3>
<div className="space-y-4">
<div className="space-y-2">
<Label className="text-base flex items-center gap-2 text-slate-700 dark:text-slate-300">
<Key className="h-4 w-4 text-indigo-600 dark:text-indigo-400" />
{t("dashboard.settings.ai.doubao.apiKey")}
<a
href="https://www.doubao.com/"
target="_blank"
rel="noopener noreferrer"
className="text-xs text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 hover:underline inline-flex items-center"
>
{t("dashboard.settings.aiConfig.apiKey.description")}
<ExternalLink className="h-3 w-3 ml-1" />
</a>
</Label>
<Input
id="doubaoApiKey"
value={doubaoApiKey}
onChange={(e) => handleApiKeyChange(e, "doubao")}
type="password"
className="h-11 bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-800 focus-visible:ring-indigo-500"
placeholder={t(
"dashboard.settings.aiConfig.apiKey.placeholder"
)}
/>
</div>
<div className="space-y-2">
<Label className="text-base flex items-center gap-2 text-slate-700 dark:text-slate-300">
<Hash className="h-4 w-4 text-indigo-600 dark:text-indigo-400" />
{t("dashboard.settings.ai.doubao.modelId")}
</Label>
<Input
id="doubaoModelId"
value={doubaoModelId}
onChange={(e) => handleModelIdChange(e, "doubao")}
className="h-11 bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-800 focus-visible:ring-indigo-500"
placeholder={t(
"dashboard.settings.aiConfig.modelId.placeholder"
)}
/>
</div>
</div>
</div>
<div className="space-y-3">
<Label className="flex items-center gap-2 text-sm font-medium">
<div className="p-1.5 rounded-md bg-purple-50 dark:bg-purple-950">
<Hash className="h-4 w-4 text-purple-500" />
</div>
{t("dashboard.settings.aiConfig.modelId.label")}
</Label>
<Input
id="doubaoModelId"
type="text"
value={doubaoModelId}
onChange={handleModelIdChange}
placeholder={t(
"dashboard.settings.aiConfig.modelId.placeholder"
)}
className="font-mono bg-muted/30"
/>
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<span>
{t("dashboard.settings.aiConfig.modelId.description")}
</span>
<ExternalLink className="h-3 w-3" />
<div
className={cn(
"space-y-6 rounded-xl p-6 transition-all duration-300",
selectedModel === "deepseek"
? "bg-gradient-to-br from-slate-50 to-white dark:from-slate-900 dark:to-slate-900/50 border border-indigo-100 dark:border-indigo-500/20 shadow-lg shadow-indigo-500/5"
: "bg-slate-50/50 dark:bg-slate-900/50 border border-slate-200 dark:border-slate-800/50 opacity-50"
)}
>
<h3 className="text-lg font-semibold flex items-center gap-2 text-slate-800 dark:text-slate-200">
<Bot className="h-5 w-5 text-indigo-600 dark:text-indigo-400" />
{t("dashboard.settings.ai.deepseek.title")}
</h3>
<div className="space-y-4">
<div className="space-y-2">
<Label className="text-base flex items-center gap-2 text-slate-700 dark:text-slate-300">
<Key className="h-4 w-4 text-indigo-600 dark:text-indigo-400" />
{t("dashboard.settings.ai.deepseek.apiKey")}
<a
href="https://platform.deepseek.com/"
target="_blank"
rel="noopener noreferrer"
className="text-xs text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 hover:underline inline-flex items-center"
>
{t("dashboard.settings.aiConfig.apiKey.description")}
<ExternalLink className="h-3 w-3 ml-1" />
</a>
</Label>
<Input
id="deepseekApiKey"
value={deepseekApiKey}
onChange={(e) => handleApiKeyChange(e, "deepseek")}
type="password"
className="h-11 bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-800 focus-visible:ring-indigo-500"
placeholder={t(
"dashboard.settings.aiConfig.apiKey.placeholder"
)}
/>
</div>
<div className="space-y-2">
<Label className="text-base flex items-center gap-2 text-slate-700 dark:text-slate-300">
<Hash className="h-4 w-4 text-indigo-600 dark:text-indigo-400" />
{t("dashboard.settings.ai.deepseek.modelId")}
</Label>
<Input
id="deepseekModelId"
value={deepseekModelId}
onChange={(e) => handleModelIdChange(e, "deepseek")}
className="h-11 bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-800 focus-visible:ring-indigo-500"
placeholder={t(
"dashboard.settings.aiConfig.modelId.placeholder"
)}
/>
</div>
</div>
</div>
@@ -1,4 +1,6 @@
"use client";
import { useTranslations } from "next-intl";
import { AlertCircle } from "lucide-react";
import { motion } from "framer-motion";
import { useRouter } from "next/navigation";
import { Input } from "@/components/ui/input";
@@ -7,16 +9,12 @@ import ThemeToggle from "../shared/ThemeToggle";
import { useResumeStore } from "@/store/useResumeStore";
import { getThemeConfig } from "@/theme/themeConfig";
import { useGrammarCheck } from "@/hooks/useGrammarCheck";
import { useEffect } from "react";
import { toast } from "sonner";
import {
HoverCard,
HoverCardTrigger,
HoverCardContent,
} from "@/components/ui/hover-card";
import { AlertCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useTranslations } from "next-intl";
interface EditorHeaderProps {
isMobile?: boolean;
@@ -22,6 +22,7 @@ import { cn } from "@/lib/utils";
import { useGrammarCheck } from "@/hooks/useGrammarCheck";
import { useAIConfigStore } from "@/store/useAIConfigStore";
import { useTranslations } from "next-intl";
import { AI_MODEL_CONFIGS } from "@/config/ai";
export type IconProps = React.HTMLAttributes<SVGElement>;
@@ -54,11 +55,25 @@ export const PreviewDock = ({
const router = useRouter();
const t = useTranslations("previewDock");
const { checkGrammar, isChecking } = useGrammarCheck();
const { doubaoApiKey, doubaoModelId } = useAIConfigStore();
const {
selectedModel,
doubaoApiKey,
doubaoModelId,
deepseekApiKey,
deepseekModelId,
} = useAIConfigStore();
const handleGrammarCheck = useCallback(async () => {
if (!resumeContentRef.current) return;
const config = AI_MODEL_CONFIGS[selectedModel];
const isConfigured =
selectedModel === "doubao"
? doubaoApiKey && doubaoModelId
: config.requiresModelId
? deepseekApiKey && deepseekModelId
: deepseekApiKey;
if (!doubaoApiKey || !doubaoModelId) {
if (!isConfigured) {
toast.error(
<>
<span>{t("grammarCheck.configurePrompt")}</span>
@@ -79,7 +94,15 @@ export const PreviewDock = ({
} catch (error) {
toast.error(t("grammarCheck.errorToast"));
}
}, [resumeContentRef, doubaoApiKey, doubaoModelId, t]);
}, [
resumeContentRef,
selectedModel,
doubaoApiKey,
doubaoModelId,
deepseekApiKey,
deepseekModelId,
t,
]);
const handleGoGitHub = () => {
window.open(GITHUB_REPO_URL, "_blank");
@@ -12,6 +12,7 @@ import {
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { useAIConfigStore } from "@/store/useAIConfigStore";
import { AI_MODEL_CONFIGS } from "@/config/ai";
import { cn } from "@/lib/utils";
interface AIPolishDialogProps {
@@ -29,12 +30,32 @@ export default function AIPolishDialog({
}: AIPolishDialogProps) {
const [isPolishing, setIsPolishing] = useState(false);
const [polishedContent, setPolishedContent] = useState("");
const { doubaoApiKey, doubaoModelId } = useAIConfigStore();
const {
selectedModel,
doubaoApiKey,
doubaoModelId,
deepseekApiKey,
deepseekModelId,
} = useAIConfigStore();
const abortControllerRef = useRef<AbortController | null>(null);
const polishedContentRef = useRef<HTMLDivElement>(null);
const handlePolish = async () => {
try {
const config = AI_MODEL_CONFIGS[selectedModel];
const isConfigured =
selectedModel === "doubao"
? doubaoApiKey && doubaoModelId
: config.requiresModelId
? deepseekApiKey && deepseekModelId
: deepseekApiKey;
if (!isConfigured) {
toast.error("请先配置 AI 模型");
onOpenChange(false);
return;
}
setIsPolishing(true);
setPolishedContent("");
@@ -47,8 +68,14 @@ export default function AIPolishDialog({
},
body: JSON.stringify({
content,
apiKey: doubaoApiKey,
model: doubaoModelId,
apiKey: selectedModel === "doubao" ? doubaoApiKey : deepseekApiKey,
model:
selectedModel === "doubao"
? doubaoModelId
: config.requiresModelId
? deepseekModelId
: config.defaultModel,
modelType: selectedModel,
}),
signal: abortControllerRef.current.signal,
});
+28
View File
@@ -0,0 +1,28 @@
import { AIModelType } from "@/store/useAIConfigStore";
export interface AIModelConfig {
url: string;
requiresModelId: boolean;
defaultModel?: string;
headers: (apiKey: string) => Record<string, string>;
}
export const AI_MODEL_CONFIGS: Record<AIModelType, AIModelConfig> = {
doubao: {
url: "https://ark.cn-beijing.volces.com/api/v3/chat/completions",
requiresModelId: true,
headers: (apiKey: string) => ({
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
}),
},
deepseek: {
url: "https://api.deepseek.com/v1/chat/completions",
requiresModelId: false,
defaultModel: "deepseek-chat",
headers: (apiKey: string) => ({
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
}),
},
};
+20
View File
@@ -79,6 +79,26 @@
"placeholder": "Please enter Doubao Model ID",
"description": "Obtain Model ID from Doubao Open Platform"
}
},
"sync": {
"title": "Sync Directory",
"description": "Choose a folder to sync and backup your resumes.",
"select": "Select Folder"
},
"ai": {
"title": "AI Model Configuration",
"description": "Configure AI models to provide intelligent optimization and suggestions for your resume.",
"selectedModel": "Select Model",
"doubao": {
"title": "Doubao Settings",
"apiKey": "API Key",
"modelId": "Model ID"
},
"deepseek": {
"title": "Deepseek Settings",
"apiKey": "API Key",
"modelId": "Model ID"
}
}
}
},
+20
View File
@@ -79,6 +79,26 @@
"placeholder": "请输入豆包模型 ID",
"description": "在豆包开放平台获取模型 ID"
}
},
"sync": {
"title": "同步目录",
"description": "选择一个文件夹来同步和备份您的简历。",
"select": "选择文件夹"
},
"ai": {
"title": "AI 模型配置",
"description": "配置 AI 模型,让 AI 助手为你的简历提供智能优化和建议。",
"selectedModel": "选择模型",
"doubao": {
"title": "豆包",
"apiKey": "API 密钥",
"modelId": "模型 ID"
},
"deepseek": {
"title": "Deepseek",
"apiKey": "API 密钥",
"modelId": "模型 ID"
}
}
}
},
@@ -1,20 +1,35 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
export type AIModelType = "doubao" | "deepseek";
interface AIConfigState {
selectedModel: AIModelType;
doubaoApiKey: string;
doubaoModelId: string;
deepseekApiKey: string;
deepseekModelId: string;
setSelectedModel: (model: AIModelType) => void;
setDoubaoApiKey: (apiKey: string) => void;
setDoubaoModelId: (modelId: string) => void;
setDeepseekApiKey: (apiKey: string) => void;
setDeepseekModelId: (modelId: string) => void;
}
export const useAIConfigStore = create<AIConfigState>()(
persist(
(set) => ({
selectedModel: "doubao",
doubaoApiKey: "",
doubaoModelId: "",
deepseekApiKey: "",
deepseekModelId: "",
setSelectedModel: (model: AIModelType) => set({ selectedModel: model }),
setDoubaoApiKey: (apiKey: string) => set({ doubaoApiKey: apiKey }),
setDoubaoModelId: (modelId: string) => set({ doubaoModelId: modelId }),
setDeepseekApiKey: (apiKey: string) => set({ deepseekApiKey: apiKey }),
setDeepseekModelId: (modelId: string) =>
set({ deepseekModelId: modelId }),
}),
{
name: "ai-config-storage",
+38 -36
View File
@@ -2,6 +2,8 @@ import { create } from "zustand";
import { toast } from "sonner";
import Mark from "mark.js";
import { useAIConfigStore } from "@/store/useAIConfigStore";
import { AI_MODEL_CONFIGS } from "@/config/ai";
import { cn } from "@/lib/utils";
export interface GrammarError {
error: string;
@@ -38,7 +40,19 @@ export const useGrammarStore = create<GrammarStore>((set, get) => ({
set((state) => ({ highlightKey: state.highlightKey + 1 })),
checkGrammar: async (text: string) => {
const { doubaoApiKey, doubaoModelId } = useAIConfigStore.getState();
const {
selectedModel,
doubaoApiKey,
doubaoModelId,
deepseekApiKey,
deepseekModelId,
} = useAIConfigStore.getState();
const config = AI_MODEL_CONFIGS[selectedModel];
const apiKey = selectedModel === "doubao" ? doubaoApiKey : deepseekApiKey;
const modelId =
selectedModel === "doubao" ? doubaoModelId : deepseekModelId;
set({ isChecking: true });
try {
@@ -49,8 +63,9 @@ export const useGrammarStore = create<GrammarStore>((set, get) => ({
},
body: JSON.stringify({
content: text,
apiKey: doubaoApiKey,
model: doubaoModelId,
apiKey,
model: config.requiresModelId ? modelId : config.defaultModel,
modelType: selectedModel,
}),
});
@@ -70,6 +85,7 @@ export const useGrammarStore = create<GrammarStore>((set, get) => ({
try {
const grammarErrors = JSON.parse(aiResponse);
if (grammarErrors.errors.length === 0) {
set({ errors: [] });
toast.success("无语法错误");
return;
}
@@ -106,47 +122,33 @@ export const useGrammarStore = create<GrammarStore>((set, get) => ({
},
selectError: (index: number) => {
const state = get();
const error = state.errors[index];
const { errors } = get();
const error = errors[index];
if (!error) return;
set({ selectedErrorIndex: index });
const preview = document.getElementById("resume-preview");
if (preview) {
const marker = new Mark(preview);
marker.unmark();
if (!preview) return;
state.errors.forEach((error) => {
marker.mark(error.context || error.text || "", {
className: `grammar-error ${error.type || ""}`,
acrossElements: true,
separateWordSearch: false,
caseSensitive: true,
});
const marker = new Mark(preview);
marker.unmark();
errors.forEach((err, i) => {
marker.mark(err.context || err.text || "", {
className: cn(
"bg-yellow-200 dark:bg-yellow-900",
i === index && "bg-green-200 dark:bg-green-900"
),
});
});
const errorElements = preview.querySelectorAll(
`.grammar-error[data-markjs=true]`
);
errorElements.forEach((el) => {
el.classList.remove("active");
});
errorElements.forEach((el) => {
if (el.textContent === (error.context || error.text)) {
el.classList.add(`active-${state.highlightKey}`);
el.scrollIntoView({
behavior: "smooth",
block: "center",
});
setTimeout(() => {
el.classList.remove(`active-${state.highlightKey}`);
}, 2000);
}
const marks = preview.querySelectorAll("mark");
const selectedMark = marks[index];
if (selectedMark) {
selectedMark.scrollIntoView({
behavior: "smooth",
block: "center",
});
}
},