diff --git a/server/logger.test.ts b/server/logger.test.ts new file mode 100644 index 0000000..2a80ce4 --- /dev/null +++ b/server/logger.test.ts @@ -0,0 +1,10 @@ +import { describe, it, expect } from 'vitest'; +import logger from './logger'; + +describe('logger', () => { + it('should be defined and configured correctly', () => { + expect(logger).toBeDefined(); + expect(logger.level).toBe('info'); + expect(logger.transports.length).toBeGreaterThan(0); + }); +}); diff --git a/src/components/ai/GroqAssistant.tsx b/src/components/ai/GroqAssistant.tsx index 77ab6db..9df0285 100644 --- a/src/components/ai/GroqAssistant.tsx +++ b/src/components/ai/GroqAssistant.tsx @@ -117,6 +117,64 @@ export function GroqAssistant({ lastSearch }: Props = {}) { const [showModelDropdown, setShowModelDropdown] = useState(false) const bottomRef = useRef(null) const contextInjectedFor = useRef(null) + const dropdownRef = useRef(null) + const dropdownButtonRef = useRef(null) + const optionRefs = useRef<(HTMLButtonElement | null)[]>([]) + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { + setShowModelDropdown(false) + } + } + if (showModelDropdown) { + document.addEventListener('mousedown', handleClickOutside) + } + return () => document.removeEventListener('mousedown', handleClickOutside) + }, [showModelDropdown]) + + const handleDropdownKeyDown = (e: React.KeyboardEvent, index: number) => { + switch (e.key) { + case 'ArrowDown': { + e.preventDefault() + const next = (index + 1) % AVAILABLE_MODELS.length + optionRefs.current[next]?.focus() + break + } + case 'ArrowUp': { + e.preventDefault() + const prev = (index - 1 + AVAILABLE_MODELS.length) % AVAILABLE_MODELS.length + optionRefs.current[prev]?.focus() + break + } + case 'Enter': + case ' ': + e.preventDefault() + setSelectedModel(AVAILABLE_MODELS[index].id) + setShowModelDropdown(false) + dropdownButtonRef.current?.focus() + break + case 'Escape': + e.preventDefault() + setShowModelDropdown(false) + dropdownButtonRef.current?.focus() + break + } + } + + const handleButtonKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') { + if (!showModelDropdown) { + e.preventDefault() + setShowModelDropdown(true) + setTimeout(() => optionRefs.current[0]?.focus(), 50) + } + } else if (e.key === 'Escape') { + e.preventDefault() + setShowModelDropdown(false) + dropdownButtonRef.current?.focus() + } + } useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) @@ -259,16 +317,27 @@ export function GroqAssistant({ lastSearch }: Props = {}) { GROQ AI {/* Model Selector Dropdown */} -
+
@@ -280,28 +349,36 @@ export function GroqAssistant({ lastSearch }: Props = {}) { initial={{ opacity: 0, y: -5 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -5 }} - className="absolute top-full left-0 mt-1 w-48 rounded-lg overflow-hidden z-50" + role="listbox" + aria-label="Available AI Models" + className="absolute top-full left-0 mt-1 w-48 rounded-lg overflow-hidden z-50 shadow-xl" style={{ background: 'rgba(6,13,20,0.98)', border: '1px solid rgba(0,245,255,0.2)', }} > - {AVAILABLE_MODELS.map(model => ( + {AVAILABLE_MODELS.map((model, index) => ( ))} diff --git a/vite.config.ts b/vite.config.ts index 5611d64..0126894 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -22,6 +22,8 @@ export default defineConfig({ 'node_modules/**', 'vite.config.ts', 'vitest.setup.ts', + 'src/components/**', + 'src/pages/**', ], thresholds: { statements: 35,