blog / src /client /contexts /AnalysisContext.tsx
hadadrjt's picture
blog: Bump to 0.0.3 version.
47f204a
//
// SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
// SPDX-License-Identifier: Apache-2.0
//
import { createContext, useContext, useState, useCallback, useMemo, useRef, useEffect } from "react";
import type { ReactNode } from "react";
import type { AnalysisState, AIStreamChunk } from "@shared/types";
const initialAnalysisState: AnalysisState = {
isOpen: false,
isLoading: false,
reasoning: "",
content: "",
isReasoningComplete: false,
error: null,
};
interface AnalysisContextValue {
isBlurred: boolean;
analysisState: AnalysisState;
postContent: string | null;
postTitle: string | null;
setPostInfo: (content: string | null, title: string | null) => void;
startAnalysis: () => Promise<void>;
resetAnalysis: () => void;
}
const AnalysisContext = createContext<AnalysisContextValue | null>(null);
interface AnalysisProviderProps {
children: ReactNode;
}
export const AnalysisProvider = ({ children }: AnalysisProviderProps): JSX.Element => {
const [analysisState, setAnalysisState] = useState<AnalysisState>(initialAnalysisState);
const [postContent, setPostContent] = useState<string | null>(null);
const [postTitle, setPostTitle] = useState<string | null>(null);
const abortControllerRef = useRef<AbortController | null>(null);
const isBlurred = analysisState.isOpen;
const setPostInfo = useCallback((content: string | null, title: string | null): void => {
setPostContent(content);
setPostTitle(title);
}, []);
const resetAnalysis = useCallback((): void => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
abortControllerRef.current = null;
}
setAnalysisState(initialAnalysisState);
}, []);
const startAnalysis = useCallback(async (): Promise<void> => {
if (!postContent || !postTitle) {
return;
}
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
abortControllerRef.current = new AbortController();
setAnalysisState({
isOpen: true,
isLoading: true,
reasoning: "",
content: "",
isReasoningComplete: false,
error: null,
});
try {
const response = await fetch("/api/analyze", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ content: postContent, title: postTitle }),
signal: abortControllerRef.current.signal,
});
if (!response.ok) {
throw new Error("Failed to start analysis");
}
const reader = response.body?.getReader();
if (!reader) {
throw new Error("No response stream");
}
const decoder = new TextDecoder("utf-8");
let buffer = "";
let hasReceivedContent = false;
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
const trimmedLine = line.trim();
if (!trimmedLine || !trimmedLine.startsWith("data: ")) {
continue;
}
const dataContent = trimmedLine.slice(6);
try {
const chunk: AIStreamChunk = JSON.parse(dataContent);
if (chunk.type === "error") {
setAnalysisState((previous) => ({
...previous,
isLoading: false,
error: chunk.content || "An error occurred",
}));
return;
}
if (chunk.type === "done") {
setAnalysisState((previous) => ({
...previous,
isLoading: false,
isReasoningComplete: true,
}));
return;
}
if (chunk.type === "reasoning" && chunk.content) {
setAnalysisState((previous) => ({
...previous,
reasoning: previous.reasoning + chunk.content,
}));
}
if (chunk.type === "content" && chunk.content) {
if (!hasReceivedContent) {
hasReceivedContent = true;
setAnalysisState((previous) => ({
...previous,
isReasoningComplete: true,
}));
}
setAnalysisState((previous) => ({
...previous,
content: previous.content + chunk.content,
}));
}
} catch {
continue;
}
}
}
setAnalysisState((previous) => ({
...previous,
isLoading: false,
isReasoningComplete: true,
}));
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
return;
}
setAnalysisState((previous) => ({
...previous,
isLoading: false,
error: error instanceof Error ? error.message : "An error occurred",
}));
}
}, [postContent, postTitle]);
useEffect((): (() => void) => {
const handlePopState = (): void => {
if (analysisState.isOpen) {
resetAnalysis();
}
};
window.addEventListener("popstate", handlePopState);
return (): void => {
window.removeEventListener("popstate", handlePopState);
};
}, [analysisState.isOpen, resetAnalysis]);
useEffect((): (() => void) => {
return (): void => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
abortControllerRef.current = null;
}
};
}, []);
const contextValue = useMemo((): AnalysisContextValue => ({
isBlurred,
analysisState,
postContent,
postTitle,
setPostInfo,
startAnalysis,
resetAnalysis,
}), [isBlurred, analysisState, postContent, postTitle, setPostInfo, startAnalysis, resetAnalysis]);
return (
<AnalysisContext.Provider value={contextValue}>
{children}
</AnalysisContext.Provider>
);
};
export const useAnalysisContext = (): AnalysisContextValue => {
const context = useContext(AnalysisContext);
if (!context) {
throw new Error("useAnalysisContext must be used within an AnalysisProvider");
}
return context;
};