mirror of
https://github.com/JOYCEQL/magic-resume.git
synced 2026-07-03 14:07:11 +02:00
refactor: grammar hook and store
This commit is contained in:
@@ -74,7 +74,6 @@ export const PreviewDock = ({
|
||||
try {
|
||||
const text = resumeContentRef.current.innerText;
|
||||
await checkGrammar(text);
|
||||
// toast.success("语法检查完成");
|
||||
} catch (error) {
|
||||
toast.error("语法检查失败,请重试");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { create } from "zustand";
|
||||
import { toast } from "sonner";
|
||||
import Mark from "mark.js";
|
||||
import { useAIConfigStore } from "@/store/useAIConfigStore";
|
||||
import { useGrammarStore } from "@/store/useGrammarStore";
|
||||
|
||||
export interface GrammarError {
|
||||
text: string;
|
||||
@@ -10,160 +10,16 @@ export interface GrammarError {
|
||||
suggestions: string[];
|
||||
}
|
||||
|
||||
interface GrammarState {
|
||||
errors: GrammarError[];
|
||||
isChecking: boolean;
|
||||
selectedErrorIndex: number | null;
|
||||
highlightKey: number;
|
||||
setErrors: (errors: GrammarError[]) => void;
|
||||
setIsChecking: (isChecking: boolean) => void;
|
||||
setSelectedErrorIndex: (index: number | null) => void;
|
||||
incrementHighlightKey: () => void;
|
||||
}
|
||||
|
||||
export const useGrammarStore = create<GrammarState>((set) => ({
|
||||
errors: [],
|
||||
isChecking: false,
|
||||
selectedErrorIndex: null,
|
||||
highlightKey: 0,
|
||||
setErrors: (errors) => {
|
||||
const preview = document.getElementById("resume-preview");
|
||||
if (preview) {
|
||||
const marker = new Mark(preview);
|
||||
marker.unmark();
|
||||
}
|
||||
set({ errors });
|
||||
},
|
||||
setIsChecking: (isChecking) => set({ isChecking }),
|
||||
setSelectedErrorIndex: (selectedErrorIndex) => set({ selectedErrorIndex }),
|
||||
incrementHighlightKey: () =>
|
||||
set((state) => ({ highlightKey: state.highlightKey + 1 })),
|
||||
}));
|
||||
|
||||
export const useGrammarCheck = () => {
|
||||
const {
|
||||
errors,
|
||||
isChecking,
|
||||
selectedErrorIndex,
|
||||
highlightKey,
|
||||
setErrors,
|
||||
setIsChecking,
|
||||
setSelectedErrorIndex,
|
||||
checkGrammar,
|
||||
clearErrors,
|
||||
selectError,
|
||||
} = useGrammarStore();
|
||||
|
||||
const { doubaoApiKey, doubaoModelId } = useAIConfigStore();
|
||||
|
||||
const checkGrammar = async (text: string) => {
|
||||
setIsChecking(true);
|
||||
try {
|
||||
const response = await fetch("/api/grammar", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
apiKey: doubaoApiKey,
|
||||
model: doubaoModelId,
|
||||
content: text,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API request failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error.code === "AuthenticationError") {
|
||||
toast.error("ApiKey 或 模型Id 不正确");
|
||||
throw new Error(data.error.message);
|
||||
}
|
||||
|
||||
const aiResponse = data.choices[0]?.message?.content;
|
||||
|
||||
try {
|
||||
const grammarErrors = JSON.parse(aiResponse).errors;
|
||||
toast.success("语法检查完成");
|
||||
setErrors(grammarErrors);
|
||||
|
||||
const preview = document.getElementById("resume-preview");
|
||||
if (preview && grammarErrors.length > 0) {
|
||||
const marker = new Mark(preview);
|
||||
grammarErrors.forEach((error: GrammarError) => {
|
||||
marker.mark(error.text, {
|
||||
className: `grammar-error ${error.type}`,
|
||||
acrossElements: true,
|
||||
separateWordSearch: false,
|
||||
caseSensitive: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (parseError) {
|
||||
toast.error(`解析AI响应失败: ${parseError}`);
|
||||
setErrors([]);
|
||||
}
|
||||
} catch (error) {
|
||||
setErrors([]);
|
||||
} finally {
|
||||
setIsChecking(false);
|
||||
}
|
||||
};
|
||||
|
||||
const clearErrors = () => {
|
||||
const preview = document.getElementById("resume-preview");
|
||||
if (preview) {
|
||||
const marker = new Mark(preview);
|
||||
marker.unmark();
|
||||
}
|
||||
setErrors([]);
|
||||
setSelectedErrorIndex(null);
|
||||
};
|
||||
|
||||
const selectError = (index: number) => {
|
||||
setSelectedErrorIndex(index);
|
||||
const error = errors[index];
|
||||
console.log(error, "error");
|
||||
if (!error) return;
|
||||
|
||||
const preview = document.getElementById("resume-preview");
|
||||
if (preview) {
|
||||
if (preview && errors.length > 0) {
|
||||
const marker = new Mark(preview);
|
||||
errors.forEach((error: GrammarError) => {
|
||||
marker.mark(error.text, {
|
||||
className: `grammar-error ${error.type}`,
|
||||
acrossElements: true,
|
||||
separateWordSearch: false,
|
||||
caseSensitive: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const errorElements = preview.querySelectorAll(
|
||||
`.grammar-error[data-markjs=true]`
|
||||
);
|
||||
|
||||
errorElements.forEach((el) => {
|
||||
el.classList.remove("active");
|
||||
});
|
||||
|
||||
errorElements.forEach((el) => {
|
||||
if (el.textContent === error.text) {
|
||||
el.classList.add(`active-${highlightKey}`);
|
||||
|
||||
el.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
el.classList.remove(`active-${highlightKey}`);
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
errors,
|
||||
isChecking,
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import { create } from "zustand";
|
||||
import { toast } from "sonner";
|
||||
import Mark from "mark.js";
|
||||
import { useAIConfigStore } from "@/store/useAIConfigStore";
|
||||
|
||||
export interface GrammarError {
|
||||
error: string;
|
||||
suggestion: string;
|
||||
context: string;
|
||||
type?: string;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
interface GrammarStore {
|
||||
isChecking: boolean;
|
||||
errors: GrammarError[];
|
||||
selectedErrorIndex: number | null;
|
||||
highlightKey: number;
|
||||
setErrors: (errors: GrammarError[]) => void;
|
||||
setIsChecking: (isChecking: boolean) => void;
|
||||
setSelectedErrorIndex: (index: number | null) => void;
|
||||
incrementHighlightKey: () => void;
|
||||
checkGrammar: (text: string) => Promise<void>;
|
||||
clearErrors: () => void;
|
||||
selectError: (index: number) => void;
|
||||
}
|
||||
|
||||
export const useGrammarStore = create<GrammarStore>((set, get) => ({
|
||||
isChecking: false,
|
||||
errors: [],
|
||||
selectedErrorIndex: null,
|
||||
highlightKey: 0,
|
||||
|
||||
setErrors: (errors) => set({ errors }),
|
||||
setIsChecking: (isChecking) => set({ isChecking }),
|
||||
setSelectedErrorIndex: (selectedErrorIndex) => set({ selectedErrorIndex }),
|
||||
incrementHighlightKey: () =>
|
||||
set((state) => ({ highlightKey: state.highlightKey + 1 })),
|
||||
|
||||
checkGrammar: async (text: string) => {
|
||||
const { doubaoApiKey, doubaoModelId } = useAIConfigStore.getState();
|
||||
set({ isChecking: true });
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/grammar", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: text,
|
||||
apiKey: doubaoApiKey,
|
||||
model: doubaoModelId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API request failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error?.code === "AuthenticationError") {
|
||||
toast.error("ApiKey 或 模型Id 不正确");
|
||||
throw new Error(data.error.message);
|
||||
}
|
||||
|
||||
const aiResponse = data.choices[0]?.message?.content;
|
||||
|
||||
try {
|
||||
const grammarErrors = JSON.parse(aiResponse);
|
||||
toast.success("语法检查完成");
|
||||
set({ errors: grammarErrors });
|
||||
|
||||
const preview = document.getElementById("resume-preview");
|
||||
if (preview) {
|
||||
const marker = new Mark(preview);
|
||||
marker.unmark();
|
||||
grammarErrors.forEach((error: GrammarError) => {
|
||||
marker.mark(error.context || error.text || "", {
|
||||
className: "bg-yellow-200 dark:bg-yellow-900",
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (parseError) {
|
||||
toast.error(`解析AI响应失败: ${parseError}`);
|
||||
set({ errors: [] });
|
||||
}
|
||||
} catch (error) {
|
||||
set({ errors: [] });
|
||||
} finally {
|
||||
set({ isChecking: false });
|
||||
}
|
||||
},
|
||||
|
||||
clearErrors: () => {
|
||||
const preview = document.getElementById("resume-preview");
|
||||
if (preview) {
|
||||
const marker = new Mark(preview);
|
||||
marker.unmark();
|
||||
}
|
||||
set({ errors: [], selectedErrorIndex: null });
|
||||
},
|
||||
|
||||
selectError: (index: number) => {
|
||||
const state = get();
|
||||
const error = state.errors[index];
|
||||
if (!error) return;
|
||||
|
||||
set({ selectedErrorIndex: index });
|
||||
|
||||
const preview = document.getElementById("resume-preview");
|
||||
if (preview) {
|
||||
const marker = new Mark(preview);
|
||||
marker.unmark();
|
||||
|
||||
state.errors.forEach((error) => {
|
||||
marker.mark(error.context || error.text || "", {
|
||||
className: `grammar-error ${error.type || ""}`,
|
||||
acrossElements: true,
|
||||
separateWordSearch: false,
|
||||
caseSensitive: true,
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user