-
Notifications
You must be signed in to change notification settings - Fork 11
refactor vector search to chat widget #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import { createServerFn } from "@tanstack/react-start"; | ||
| import { adminMiddleware } from "~/lib/auth"; | ||
| import { z } from "zod"; | ||
| import { ragChatUseCase } from "~/use-cases/rag-chat"; | ||
|
|
||
| const videoSourceSchema = z.object({ | ||
| segmentId: z.number(), | ||
| segmentTitle: z.string(), | ||
| segmentSlug: z.string(), | ||
| moduleTitle: z.string(), | ||
| chunkText: z.string(), | ||
| similarity: z.number(), | ||
| }); | ||
|
|
||
| const MAX_MESSAGE_CONTENT_LENGTH = 2000; | ||
|
|
||
| const conversationMessageSchema = z.object({ | ||
| id: z.string(), | ||
| role: z.enum(["user", "assistant"]), | ||
| content: z | ||
| .string() | ||
| .max(MAX_MESSAGE_CONTENT_LENGTH, "Message content too long"), | ||
| timestamp: z.string(), | ||
| sources: z.array(videoSourceSchema).optional(), | ||
| }); | ||
|
|
||
| const ragChatInputSchema = z.object({ | ||
| userMessage: z | ||
| .string() | ||
| .min(1, "Message cannot be empty") | ||
| .max(MAX_MESSAGE_CONTENT_LENGTH, "Message too long"), | ||
| conversationHistory: z | ||
| .array(conversationMessageSchema) | ||
| .max(20, "Too many messages in history"), | ||
| }); | ||
|
|
||
| export const ragChatFn = createServerFn({ method: "POST" }) | ||
| .middleware([adminMiddleware]) | ||
| .inputValidator(ragChatInputSchema) | ||
| .handler(async ({ data }) => { | ||
| return ragChatUseCase({ | ||
| userMessage: data.userMessage, | ||
| conversationHistory: data.conversationHistory, | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,143 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { useState, useCallback, useEffect, useRef } from "react"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { useMutation } from "@tanstack/react-query"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { ragChatFn } from "~/fn/rag-chat"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import type { VideoSource, ConversationMessage } from "~/use-cases/rag-chat"; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| export type { VideoSource, ConversationMessage }; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const STORAGE_KEY = "rag-chat-history"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| const SOURCES_STORAGE_KEY = "rag-chat-sources"; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| function loadFromStorage<T>(key: string, fallback: T): T { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof window === "undefined") return fallback; | ||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const stored = sessionStorage.getItem(key); | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (stored) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return JSON.parse(stored) as T; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(`[RAG Chat] Failed to load from sessionStorage:`, error); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| return fallback; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| function saveToStorage<T>(key: string, value: T): void { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof window === "undefined") return; | ||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||
| sessionStorage.setItem(key, JSON.stringify(value)); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(`[RAG Chat] Failed to save to sessionStorage:`, error); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| function clearStorage(): void { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof window === "undefined") return; | ||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||
| sessionStorage.removeItem(STORAGE_KEY); | ||||||||||||||||||||||||||||||||||||||||||||||||
| sessionStorage.removeItem(SOURCES_STORAGE_KEY); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(`[RAG Chat] Failed to clear sessionStorage:`, error); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| export interface UseRagChatReturn { | ||||||||||||||||||||||||||||||||||||||||||||||||
| messages: ConversationMessage[]; | ||||||||||||||||||||||||||||||||||||||||||||||||
| isLoading: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||
| error: Error | null; | ||||||||||||||||||||||||||||||||||||||||||||||||
| sendMessage: (content: string) => Promise<void>; | ||||||||||||||||||||||||||||||||||||||||||||||||
| clearChat: () => void; | ||||||||||||||||||||||||||||||||||||||||||||||||
| currentSources: VideoSource[]; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+98
to
+105
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Find the ragChatFn definition and its error type specification
rg -nP --type=ts -C5 'ragChatFn.*=.*createServerFn' src/Repository: webdevcody/agentic-jumpstart Length of output: 656 🏁 Script executed: #!/bin/bash
# Check the useMutation configuration and error handling in the hook
rg -nP --type=ts -C10 'useMutation|error.*Error|Error.*null' src/hooks/use-rag-chat.tsRepository: webdevcody/agentic-jumpstart Length of output: 2626 🏁 Script executed: #!/bin/bash
# Get a fuller picture of the hook implementation to see how error is used
head -150 src/hooks/use-rag-chat.ts | tail -80Repository: webdevcody/agentic-jumpstart Length of output: 2337 🏁 Script executed: #!/bin/bash
# Get the rest of the useRagChat hook implementation to see how error is returned
tail -60 src/hooks/use-rag-chat.tsRepository: webdevcody/agentic-jumpstart Length of output: 1609 Fix error type mismatch in UseRagChatReturn interface. The const mutation = useMutation<ReturnType, Error, string>({
mutationFn: async (userMessage: string) => { ... }
})🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| export function useRagChat(): UseRagChatReturn { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const [messages, setMessages] = useState<ConversationMessage[]>(() => | ||||||||||||||||||||||||||||||||||||||||||||||||
| loadFromStorage<ConversationMessage[]>(STORAGE_KEY, []) | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const [currentSources, setCurrentSources] = useState<VideoSource[]>(() => | ||||||||||||||||||||||||||||||||||||||||||||||||
| loadFromStorage<VideoSource[]>(SOURCES_STORAGE_KEY, []) | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const messagesRef = useRef<ConversationMessage[]>(messages); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| messagesRef.current = messages; | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, [messages]); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| saveToStorage(STORAGE_KEY, messages); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, [messages]); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| saveToStorage(SOURCES_STORAGE_KEY, currentSources); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, [currentSources]); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const mutation = useMutation({ | ||||||||||||||||||||||||||||||||||||||||||||||||
| mutationFn: async (userMessage: string) => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const result = await ragChatFn({ | ||||||||||||||||||||||||||||||||||||||||||||||||
| data: { | ||||||||||||||||||||||||||||||||||||||||||||||||
| userMessage, | ||||||||||||||||||||||||||||||||||||||||||||||||
| conversationHistory: messagesRef.current, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| return result; | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||
| onMutate: (userMessage) => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const userMsg: ConversationMessage = { | ||||||||||||||||||||||||||||||||||||||||||||||||
| id: crypto.randomUUID(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| role: "user", | ||||||||||||||||||||||||||||||||||||||||||||||||
| content: userMessage, | ||||||||||||||||||||||||||||||||||||||||||||||||
| timestamp: new Date().toISOString(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
| setMessages((prev) => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const newMessages = [...prev, userMsg]; | ||||||||||||||||||||||||||||||||||||||||||||||||
| messagesRef.current = newMessages; | ||||||||||||||||||||||||||||||||||||||||||||||||
| return newMessages; | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||
| setCurrentSources([]); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||
| onSuccess: (result) => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const assistantMsg: ConversationMessage = { | ||||||||||||||||||||||||||||||||||||||||||||||||
| id: crypto.randomUUID(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| role: "assistant", | ||||||||||||||||||||||||||||||||||||||||||||||||
| content: result.response, | ||||||||||||||||||||||||||||||||||||||||||||||||
| timestamp: new Date().toISOString(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| sources: result.sources, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
| setMessages((prev) => [...prev, assistantMsg]); | ||||||||||||||||||||||||||||||||||||||||||||||||
| setCurrentSources(result.sources); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| onError: (error) => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| setMessages((prev) => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const lastMessage = prev[prev.length - 1]; | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (lastMessage?.role === "user") { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return prev.slice(0, -1); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| return prev; | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| console.error("[RAG Chat] Error:", error); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+163
to
+172
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sync messagesRef when rolling back optimistic updates. When the error handler rolls back the optimistic user message (lines 109-115), it doesn't update 🔧 Suggested fix onError: (error) => {
setMessages((prev) => {
const lastMessage = prev[prev.length - 1];
if (lastMessage?.role === "user") {
- return prev.slice(0, -1);
+ const rolled = prev.slice(0, -1);
+ messagesRef.current = rolled;
+ return rolled;
}
+ messagesRef.current = prev;
return prev;
});
console.error("[RAG Chat] Error:", error);
},📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const sendMessage = useCallback( | ||||||||||||||||||||||||||||||||||||||||||||||||
| async (content: string) => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (!content.trim() || mutation.isPending) return; | ||||||||||||||||||||||||||||||||||||||||||||||||
| await mutation.mutateAsync(content.trim()); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| [mutation] | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const clearChat = useCallback(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| setMessages([]); | ||||||||||||||||||||||||||||||||||||||||||||||||
| setCurrentSources([]); | ||||||||||||||||||||||||||||||||||||||||||||||||
| clearStorage(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| mutation.reset(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, [mutation]); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clear chat does not prevent orphaned response from in-flight requestLow Severity When a user clicks "Clear Chat" while an API request is in flight, 🔬 Verification TestWhy verification test was not possible: This race condition requires mocking React Query's mutation lifecycle timing and simulating user interaction mid-request. The bug manifests when: (1) a mutation is pending, (2) clearChat is called, (3) the mutation completes. Testing would require a full React rendering environment with React Query and precise timing control, which cannot be done through simple unit tests without the complete application context. Additional Locations (1) |
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||
| messages, | ||||||||||||||||||||||||||||||||||||||||||||||||
| isLoading: mutation.isPending, | ||||||||||||||||||||||||||||||||||||||||||||||||
| error: mutation.error, | ||||||||||||||||||||||||||||||||||||||||||||||||
| sendMessage, | ||||||||||||||||||||||||||||||||||||||||||||||||
| clearChat, | ||||||||||||||||||||||||||||||||||||||||||||||||
| currentSources, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| import OpenAI from "openai"; | ||
| import { env } from "~/utils/env"; | ||
|
|
||
| const openai = new OpenAI({ | ||
| apiKey: env.OPENAI_API_KEY, | ||
| }); | ||
|
|
||
| const CHAT_MODEL = "gpt-4o"; | ||
| const MAX_RETRIES = 3; | ||
| const INITIAL_RETRY_DELAY_MS = 1000; | ||
|
|
||
| export interface ChatMessage { | ||
| role: "system" | "user" | "assistant"; | ||
| content: string; | ||
| } | ||
|
|
||
| export interface ChatCompletionOptions { | ||
| model?: "gpt-4o-mini" | "gpt-4o"; | ||
| temperature?: number; | ||
| maxTokens?: number; | ||
| } | ||
|
|
||
| export interface ChatCompletionResult { | ||
| content: string; | ||
| usage?: { | ||
| promptTokens: number; | ||
| completionTokens: number; | ||
| totalTokens: number; | ||
| }; | ||
| } | ||
|
|
||
| class ChatCompletionError extends Error { | ||
| constructor( | ||
| message: string, | ||
| public readonly code?: string, | ||
| public readonly status?: number, | ||
| public readonly context?: Record<string, unknown> | ||
| ) { | ||
| super(message); | ||
| this.name = "ChatCompletionError"; | ||
| } | ||
| } | ||
|
|
||
| async function sleep(ms: number): Promise<void> { | ||
| return new Promise((resolve) => setTimeout(resolve, ms)); | ||
| } | ||
|
|
||
| async function withRetry<T>( | ||
| fn: () => Promise<T>, | ||
| context: Record<string, unknown> | ||
| ): Promise<T> { | ||
| let lastError: Error | undefined; | ||
|
|
||
| for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { | ||
| try { | ||
| return await fn(); | ||
| } catch (error) { | ||
| lastError = error instanceof Error ? error : new Error(String(error)); | ||
|
|
||
| const isRetryable = | ||
| error instanceof OpenAI.APIError && | ||
| (error.status === 429 || | ||
| error.status === 500 || | ||
| error.status === 502 || | ||
| error.status === 503); | ||
|
|
||
| if (!isRetryable || attempt === MAX_RETRIES - 1) { | ||
| break; | ||
| } | ||
|
|
||
| const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt); | ||
| console.warn( | ||
| `Chat completion API call failed (attempt ${attempt + 1}/${MAX_RETRIES}), retrying in ${delay}ms...`, | ||
| { error: lastError.message, ...context } | ||
| ); | ||
| await sleep(delay); | ||
| } | ||
| } | ||
|
|
||
| if (lastError instanceof OpenAI.APIError) { | ||
| throw new ChatCompletionError( | ||
| `OpenAI API error: ${lastError.message}`, | ||
| lastError.code ?? undefined, | ||
| lastError.status, | ||
| context | ||
| ); | ||
| } | ||
|
|
||
| throw new ChatCompletionError( | ||
| `Chat completion failed: ${lastError?.message ?? "Unknown error"}`, | ||
| undefined, | ||
| undefined, | ||
| context | ||
| ); | ||
| } | ||
|
|
||
| export async function createChatCompletion( | ||
| messages: ChatMessage[], | ||
| options?: ChatCompletionOptions | ||
| ): Promise<ChatCompletionResult> { | ||
| if (!messages || !Array.isArray(messages) || messages.length === 0) { | ||
| throw new ChatCompletionError( | ||
| "Messages must be a non-empty array", | ||
| undefined, | ||
| undefined, | ||
| { messagesLength: messages?.length ?? 0 } | ||
| ); | ||
| } | ||
|
|
||
| const model = options?.model ?? CHAT_MODEL; | ||
| const temperature = options?.temperature ?? 0.7; | ||
| const maxTokens = options?.maxTokens ?? 2048; | ||
|
|
||
| return withRetry( | ||
| async () => { | ||
| const response = await openai.chat.completions.create({ | ||
| model, | ||
| messages, | ||
| temperature, | ||
| max_tokens: maxTokens, | ||
| }); | ||
|
|
||
| const choice = response.choices[0]; | ||
| if (!choice?.message?.content) { | ||
| throw new ChatCompletionError( | ||
| "Invalid API response: missing message content", | ||
| undefined, | ||
| undefined, | ||
| { choicesLength: response.choices.length } | ||
| ); | ||
| } | ||
|
|
||
| return { | ||
| content: choice.message.content, | ||
| usage: response.usage | ||
| ? { | ||
| promptTokens: response.usage.prompt_tokens, | ||
| completionTokens: response.usage.completion_tokens, | ||
| totalTokens: response.usage.total_tokens, | ||
| } | ||
| : undefined, | ||
| }; | ||
| }, | ||
| { model, messagesCount: messages.length } | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add runtime validation for loaded sessionStorage data.
The type assertion
as Ton line 16 is unsafe without validating that the parsed data actually matches the expected structure. If sessionStorage contains corrupted or tampered data, this could cause runtime errors when the hook or components try to use the loaded data.Consider adding runtime validation using Zod schemas or type guards to ensure the loaded data matches the expected structure before returning it.
🛡️ Suggested validation approach
You could create Zod schemas for the stored data types and validate before returning:
🤖 Prompt for AI Agents