mirror of
https://github.com/JOYCEQL/magic-resume.git
synced 2026-06-01 23:38:48 +02:00
refactor:ai config
This commit is contained in:
@@ -1,18 +1,18 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { AIModelType } from "@/store/useAIConfigStore";
|
||||
import { AIModelType } from "@/config/ai";
|
||||
import { AI_MODEL_CONFIGS } from "@/config/ai";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { apiKey, model, content, modelType } = body;
|
||||
const { apiKey, model, content, modelType, apiEndpoint } = body;
|
||||
|
||||
const modelConfig = AI_MODEL_CONFIGS[modelType as AIModelType];
|
||||
if (!modelConfig) {
|
||||
throw new Error("Invalid model type");
|
||||
}
|
||||
|
||||
const response = await fetch(modelConfig.url, {
|
||||
const response = await fetch(modelConfig.url(apiEndpoint), {
|
||||
method: "POST",
|
||||
headers: modelConfig.headers(apiKey),
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { AIModelType } from "@/store/useAIConfigStore";
|
||||
import { AIModelType } from "@/config/ai";
|
||||
import { AI_MODEL_CONFIGS } from "@/config/ai";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { CalendarIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -17,8 +15,7 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import RichTextEditor from "../shared/rich-editor/RichEditor";
|
||||
import AIPolishDialog from "../shared/ai/AIPolishDialog";
|
||||
import { useAIConfigStore } from "@/store/useAIConfigStore";
|
||||
import { AI_MODEL_CONFIGS } from "@/config/ai";
|
||||
import { useAIConfiguration } from "@/hooks/useAIConfiguration";
|
||||
|
||||
interface FieldProps {
|
||||
label?: string;
|
||||
@@ -43,17 +40,7 @@ const Field = ({
|
||||
const [displayMonth, setDisplayMonth] = useState<Date>(new Date());
|
||||
const [fromDate, setFromDate] = useState<Date | undefined>(undefined);
|
||||
const [showPolishDialog, setShowPolishDialog] = useState(false);
|
||||
const router = useRouter();
|
||||
const {
|
||||
doubaoModelId,
|
||||
doubaoApiKey,
|
||||
selectedModel,
|
||||
deepseekApiKey,
|
||||
deepseekModelId,
|
||||
openaiApiKey,
|
||||
openaiModelId,
|
||||
openaiApiEndpoint
|
||||
} = useAIConfigStore();
|
||||
const { checkConfiguration } = useAIConfiguration();
|
||||
const t = useTranslations();
|
||||
|
||||
const currentDate = useMemo(
|
||||
@@ -254,31 +241,9 @@ const Field = ({
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
onPolish={() => {
|
||||
const config = AI_MODEL_CONFIGS[selectedModel];
|
||||
const isConfigured =
|
||||
selectedModel === "doubao"
|
||||
? doubaoApiKey && doubaoModelId
|
||||
: selectedModel === "openai"
|
||||
? openaiApiKey && openaiModelId && openaiApiEndpoint
|
||||
: config.requiresModelId
|
||||
? deepseekApiKey && deepseekModelId
|
||||
: deepseekApiKey;
|
||||
|
||||
if (!isConfigured) {
|
||||
toast.error(
|
||||
<>
|
||||
<span>{t("previewDock.grammarCheck.configurePrompt")}</span>
|
||||
<Button
|
||||
className="p-0 h-auto text-white"
|
||||
onClick={() => router.push("/app/dashboard/ai")}
|
||||
>
|
||||
{t("previewDock.grammarCheck.configureButton")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
return;
|
||||
if (checkConfiguration()) {
|
||||
setShowPolishDialog(true);
|
||||
}
|
||||
setShowPolishDialog(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -36,6 +36,7 @@ import { useGrammarCheck } from "@/hooks/useGrammarCheck";
|
||||
import { useAIConfigStore } from "@/store/useAIConfigStore";
|
||||
import { AI_MODEL_CONFIGS } from "@/config/ai";
|
||||
import { useResumeStore } from "@/store/useResumeStore";
|
||||
import { useAIConfiguration } from "@/hooks/useAIConfiguration";
|
||||
|
||||
export type IconProps = React.HTMLAttributes<SVGElement>;
|
||||
|
||||
@@ -369,30 +370,14 @@ const PreviewDock = ({
|
||||
}
|
||||
};
|
||||
|
||||
const { checkConfiguration } = useAIConfiguration();
|
||||
|
||||
// ... (keep other hooks)
|
||||
|
||||
const handleGrammarCheck = useCallback(async () => {
|
||||
if (!resumeContentRef.current) return;
|
||||
const config = AI_MODEL_CONFIGS[selectedModel];
|
||||
const isConfigured =
|
||||
selectedModel === "doubao"
|
||||
? doubaoApiKey && doubaoModelId
|
||||
: selectedModel === "openai"
|
||||
? openaiApiKey && openaiModelId && openaiApiEndpoint
|
||||
: config.requiresModelId
|
||||
? deepseekApiKey && deepseekModelId
|
||||
: deepseekApiKey;
|
||||
|
||||
if (!isConfigured) {
|
||||
toast.error(
|
||||
<>
|
||||
<span>{t("grammarCheck.configurePrompt")}</span>
|
||||
<Button
|
||||
className="p-0 h-auto text-white"
|
||||
onClick={() => router.push("/app/dashboard/ai")}
|
||||
>
|
||||
{t("grammarCheck.configureButton")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
if (!checkConfiguration()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -402,20 +387,7 @@ const PreviewDock = ({
|
||||
} catch (error) {
|
||||
toast.error(t("grammarCheck.errorToast"));
|
||||
}
|
||||
}, [
|
||||
resumeContentRef,
|
||||
selectedModel,
|
||||
doubaoApiKey,
|
||||
doubaoModelId,
|
||||
deepseekApiKey,
|
||||
deepseekModelId,
|
||||
openaiApiKey,
|
||||
openaiModelId,
|
||||
openaiApiEndpoint,
|
||||
checkGrammar,
|
||||
t,
|
||||
router
|
||||
]);
|
||||
}, [resumeContentRef, checkConfiguration, checkGrammar, t]);
|
||||
|
||||
const handleGoGitHub = () => {
|
||||
window.open(GITHUB_REPO_URL, "_blank");
|
||||
|
||||
@@ -42,23 +42,14 @@ export default function AIPolishDialog({
|
||||
openaiApiKey,
|
||||
openaiModelId,
|
||||
openaiApiEndpoint,
|
||||
isConfigured
|
||||
} = 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
|
||||
: selectedModel === "openai"
|
||||
? openaiApiKey && openaiModelId && openaiApiEndpoint
|
||||
: config.requiresModelId
|
||||
? deepseekApiKey && deepseekModelId
|
||||
: deepseekApiKey;
|
||||
|
||||
if (!isConfigured) {
|
||||
if (!isConfigured()) {
|
||||
toast.error(t("error.configRequired"));
|
||||
onOpenChange(false);
|
||||
return;
|
||||
@@ -69,6 +60,7 @@ export default function AIPolishDialog({
|
||||
|
||||
abortControllerRef.current = new AbortController();
|
||||
|
||||
const config = AI_MODEL_CONFIGS[selectedModel];
|
||||
const response = await fetch("/api/polish", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
||||
+29
-1
@@ -1,10 +1,21 @@
|
||||
import { AIModelType } from "@/store/useAIConfigStore";
|
||||
export type AIModelType = "doubao" | "deepseek" | "openai";
|
||||
|
||||
export interface AIValidationContext {
|
||||
doubaoApiKey?: string;
|
||||
doubaoModelId?: string;
|
||||
deepseekApiKey?: string;
|
||||
deepseekModelId?: string;
|
||||
openaiApiKey?: string;
|
||||
openaiModelId?: string;
|
||||
openaiApiEndpoint?: string;
|
||||
}
|
||||
|
||||
export interface AIModelConfig {
|
||||
url: (endpoint: string) => string;
|
||||
requiresModelId: boolean;
|
||||
defaultModel?: string;
|
||||
headers: (apiKey: string) => Record<string, string>;
|
||||
validate: (context: AIValidationContext) => boolean;
|
||||
}
|
||||
|
||||
export const AI_MODEL_CONFIGS: Record<AIModelType, AIModelConfig> = {
|
||||
@@ -15,6 +26,7 @@ export const AI_MODEL_CONFIGS: Record<AIModelType, AIModelConfig> = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
}),
|
||||
validate: (context: AIValidationContext) => !!(context.doubaoApiKey && context.doubaoModelId),
|
||||
},
|
||||
deepseek: {
|
||||
url: (endpoint: string) => "https://api.deepseek.com/v1/chat/completions",
|
||||
@@ -24,6 +36,21 @@ export const AI_MODEL_CONFIGS: Record<AIModelType, AIModelConfig> = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
}),
|
||||
validate: (context: AIValidationContext) =>
|
||||
context.deepseekModelId
|
||||
// If requiresModelId is false (for deepseek it is), we usually just check apiKey?
|
||||
// But looking at previous store logic:
|
||||
// requiredModelId ? (apiKey && modelId) : apiKey
|
||||
// For deepseek config above, requiresModelId is false.
|
||||
// So logic should be !!context.deepseekApiKey
|
||||
// BUT, in your previous store code:
|
||||
// config.requiresModelId ? !!(state.deepseekApiKey && state.deepseekModelId) : !!state.deepseekApiKey
|
||||
// Deepseek config has requiresModelId: false.
|
||||
// So it returns !!state.deepseekApiKey.
|
||||
// Wait, let's make it generic based on its own config if possible, or just hardcode specific logic.
|
||||
// Since we are inside the specific config, we know logic.
|
||||
? !!(context.deepseekApiKey && context.deepseekModelId)
|
||||
: !!context.deepseekApiKey,
|
||||
},
|
||||
openai: {
|
||||
url: (endpoint: string) => `${endpoint}/chat/completions`,
|
||||
@@ -32,5 +59,6 @@ export const AI_MODEL_CONFIGS: Record<AIModelType, AIModelConfig> = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
}),
|
||||
validate: (context: AIValidationContext) => !!(context.openaiApiKey && context.openaiModelId && context.openaiApiEndpoint),
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useAIConfigStore } from "@/store/useAIConfigStore";
|
||||
|
||||
export const useAIConfiguration = () => {
|
||||
const router = useRouter();
|
||||
const t = useTranslations("previewDock.grammarCheck");
|
||||
const { isConfigured, selectedModel } = useAIConfigStore();
|
||||
|
||||
const checkConfiguration = () => {
|
||||
if (!isConfigured()) {
|
||||
toast.error(
|
||||
<>
|
||||
<span>{t("configurePrompt")}</span>
|
||||
<Button
|
||||
variant="link"
|
||||
className="p-0 h-auto ml-1 font-normal"
|
||||
onClick={() => router.push("/app/dashboard/ai")}
|
||||
>
|
||||
{t("configureButton")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
return {
|
||||
checkConfiguration,
|
||||
};
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
export type AIModelType = "doubao" | "deepseek" | "openai";
|
||||
import { AI_MODEL_CONFIGS, AIModelType } from "@/config/ai";
|
||||
|
||||
interface AIConfigState {
|
||||
selectedModel: AIModelType;
|
||||
@@ -20,11 +19,12 @@ interface AIConfigState {
|
||||
setOpenaiApiKey: (apiKey: string) => void;
|
||||
setOpenaiModelId: (modelId: string) => void;
|
||||
setOpenaiApiEndpoint: (endpoint: string) => void;
|
||||
isConfigured: () => boolean;
|
||||
}
|
||||
|
||||
export const useAIConfigStore = create<AIConfigState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
(set, get) => ({
|
||||
selectedModel: "doubao",
|
||||
doubaoApiKey: "",
|
||||
doubaoModelId: "",
|
||||
@@ -40,7 +40,12 @@ export const useAIConfigStore = create<AIConfigState>()(
|
||||
setDeepseekModelId: (modelId: string) => set({ deepseekModelId: modelId }),
|
||||
setOpenaiApiKey: (apiKey: string) => set({ openaiApiKey: apiKey }),
|
||||
setOpenaiModelId: (modelId: string) => set({ openaiModelId: modelId }),
|
||||
setOpenaiApiEndpoint: (endpoint: string) => set({ openaiApiEndpoint: endpoint })
|
||||
setOpenaiApiEndpoint: (endpoint: string) => set({ openaiApiEndpoint: endpoint }),
|
||||
isConfigured: () => {
|
||||
const state = get();
|
||||
const config = AI_MODEL_CONFIGS[state.selectedModel];
|
||||
return config.validate(state);
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: "ai-config-storage"
|
||||
|
||||
Reference in New Issue
Block a user