diff --git a/src/app/api/grammar/route.ts b/src/app/api/grammar/route.ts index 47c455f..c495ca9 100644 --- a/src/app/api/grammar/route.ts +++ b/src/app/api/grammar/route.ts @@ -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), diff --git a/src/app/api/polish/route.ts b/src/app/api/polish/route.ts index ae5188e..4a6efb7 100644 --- a/src/app/api/polish/route.ts +++ b/src/app/api/polish/route.ts @@ -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) { diff --git a/src/components/editor/Field.tsx b/src/components/editor/Field.tsx index e7bbeba..9bcce56 100644 --- a/src/components/editor/Field.tsx +++ b/src/components/editor/Field.tsx @@ -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(new Date()); const [fromDate, setFromDate] = useState(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( - <> - {t("previewDock.grammarCheck.configurePrompt")} - - - ); - return; + if (checkConfiguration()) { + setShowPolishDialog(true); } - setShowPolishDialog(true); }} /> diff --git a/src/components/preview/PreviewDock.tsx b/src/components/preview/PreviewDock.tsx index 4bdc1ff..1392069 100644 --- a/src/components/preview/PreviewDock.tsx +++ b/src/components/preview/PreviewDock.tsx @@ -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; @@ -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( - <> - {t("grammarCheck.configurePrompt")} - - - ); + + 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"); diff --git a/src/components/shared/ai/AIPolishDialog.tsx b/src/components/shared/ai/AIPolishDialog.tsx index 0eb6522..d8b22e5 100644 --- a/src/components/shared/ai/AIPolishDialog.tsx +++ b/src/components/shared/ai/AIPolishDialog.tsx @@ -42,23 +42,14 @@ export default function AIPolishDialog({ openaiApiKey, openaiModelId, openaiApiEndpoint, + isConfigured } = useAIConfigStore(); const abortControllerRef = useRef(null); const polishedContentRef = useRef(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: { diff --git a/src/config/ai.ts b/src/config/ai.ts index dd5b4fc..6e8159b 100644 --- a/src/config/ai.ts +++ b/src/config/ai.ts @@ -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; + validate: (context: AIValidationContext) => boolean; } export const AI_MODEL_CONFIGS: Record = { @@ -15,6 +26,7 @@ export const AI_MODEL_CONFIGS: Record = { "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 = { "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 = { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, }), + validate: (context: AIValidationContext) => !!(context.openaiApiKey && context.openaiModelId && context.openaiApiEndpoint), } }; diff --git a/src/hooks/useAIConfiguration.tsx b/src/hooks/useAIConfiguration.tsx new file mode 100644 index 0000000..a75d765 --- /dev/null +++ b/src/hooks/useAIConfiguration.tsx @@ -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( + <> + {t("configurePrompt")} + + + ); + return false; + } + + return true; + }; + + return { + checkConfiguration, + }; +}; diff --git a/src/store/useAIConfigStore.ts b/src/store/useAIConfigStore.ts index 4ed5f63..150dd78 100644 --- a/src/store/useAIConfigStore.ts +++ b/src/store/useAIConfigStore.ts @@ -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()( persist( - (set) => ({ + (set, get) => ({ selectedModel: "doubao", doubaoApiKey: "", doubaoModelId: "", @@ -40,7 +40,12 @@ export const useAIConfigStore = create()( 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"