refactor:ai config

This commit is contained in:
JOYCEQL
2026-02-04 22:15:56 +08:00
parent d6f0c4ba93
commit 43430cd72e
8 changed files with 92 additions and 95 deletions
+3 -3
View File
@@ -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 -1
View File
@@ -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) {
+4 -39
View File
@@ -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>
+8 -36
View File
@@ -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");
+3 -11
View File
@@ -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
View File
@@ -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),
}
};
+35
View File
@@ -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,
};
};
+9 -4
View File
@@ -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"