diff --git a/frontend/app/api/ai/explain/route.ts b/frontend/app/api/ai/explain/route.ts index cebfff7e..bbcdb192 100644 --- a/frontend/app/api/ai/explain/route.ts +++ b/frontend/app/api/ai/explain/route.ts @@ -5,6 +5,7 @@ import { createExplainPrompt, type ExplanationResponse, } from '@/lib/ai/prompts'; +import { getClientIp } from '@/lib/security/rate-limit'; const rateLimiter = new Map(); const MAX_REQUESTS_PER_WINDOW = 10; @@ -36,13 +37,6 @@ const requestSchema = z.object({ .optional(), }); -function getClientIp(request: NextRequest): string { - const forwarded = request.headers.get('x-forwarded-for'); - if (forwarded) { - return forwarded.split(',')[0].trim(); - } - return request.headers.get('x-real-ip') || 'unknown'; -} function checkRateLimit(ip: string): { allowed: boolean; remaining: number; resetIn: number } { cleanupRateLimiter(); @@ -104,7 +98,7 @@ export async function POST(request: NextRequest) { ); } - const clientIp = getClientIp(request); + const clientIp = getClientIp(request) ?? 'unknown'; const rateLimit = checkRateLimit(clientIp); if (!rateLimit.allowed) { diff --git a/frontend/components/q&a/AccordionList.tsx b/frontend/components/q&a/AccordionList.tsx index 3eaa136f..93fdf257 100644 --- a/frontend/components/q&a/AccordionList.tsx +++ b/frontend/components/q&a/AccordionList.tsx @@ -20,7 +20,7 @@ import SelectableText from '@/components/q&a/SelectableText'; import FloatingExplainButton from '@/components/q&a/FloatingExplainButton'; import AIWordHelper from '@/components/q&a/AIWordHelper'; import HighlightCachedTerms from '@/components/q&a/HighlightCachedTerms'; -import { getCachedTerms } from '@/lib/ai/explainCache'; +import { getCachedTerms, CACHE_KEY } from '@/lib/ai/explainCache'; import type { AnswerBlock, BulletListBlock, @@ -315,7 +315,7 @@ export default function AccordionList({ items }: { items: QuestionEntry[] }) { useEffect(() => { const handleStorage = (e: StorageEvent) => { - if (e.key === 'ai-word-explanations') { + if (e.key === CACHE_KEY) { refreshCachedTerms(); } }; diff --git a/frontend/components/q&a/FloatingExplainButton.tsx b/frontend/components/q&a/FloatingExplainButton.tsx index 68db8a90..a5f8d0ef 100644 --- a/frontend/components/q&a/FloatingExplainButton.tsx +++ b/frontend/components/q&a/FloatingExplainButton.tsx @@ -17,15 +17,20 @@ export default function FloatingExplainButton({ }: FloatingExplainButtonProps) { const t = useTranslations('aiHelper'); const buttonRef = useRef(null); + const onDismissRef = useRef(onDismiss); + + useEffect(() => { + onDismissRef.current = onDismiss; + }); useEffect(() => { const handleScroll = () => { - onDismiss(); + onDismissRef.current(); }; const handleClickOutside = (e: MouseEvent) => { if (buttonRef.current && !buttonRef.current.contains(e.target as Node)) { - onDismiss(); + onDismissRef.current(); } }; @@ -39,7 +44,7 @@ export default function FloatingExplainButton({ window.removeEventListener('scroll', handleScroll, true); document.removeEventListener('mousedown', handleClickOutside); }; - }, [onDismiss]); + }, []); return (