Voice Search Plan implementacji
Przegląd
Dodajemy ikonę mikrofonu do taba “Wyszukaj przepis” w AddRecipeForm. Kliknięcie uruchamia Web Speech API — przeglądarka prosi o uprawnienia, rozpoznaje mowę po polsku, a rozpoznany tekst natychmiast wyzwala wyszukiwanie przez Exa. Zmiana dotyczy wyłącznie jednego komponentu klienckiego.
Analiza stanu obecnego
AddRecipeForm (src/app/components/add-recipe-form.tsx) obsługuje dwa taby: 'search' (Exa) i 'add' (URL). Stan formularza: SearchState = 'idle' | 'loading' | 'searching' | 'results' | 'error'. Input w tabie search ma ikonę Search po lewej (absolutna pozycja, pl-9). Po prawej stronie inputa jest miejsce — brak ikon, brak paddingu. Import lucide-react: Link, Search, X — brak Mic.
Web Speech API (SpeechRecognition / webkitSpeechRecognition) dostępne w Chrome i Safari, niedostępne w Firefox.
Pożądany stan końcowy
User w tabie “Wyszukaj przepis” widzi ikonę mikrofonu w prawym rogu inputa. Kliknięcie: przeglądarka pyta o uprawnienia → po akceptacji rozpoznaje mowę → wyniki Exa pojawiają się automatycznie. Przy odmowie uprawnień lub błędzie STT — krótki czerwony komunikat. W Firefoksie ikona nie jest widoczna (graceful degradation).
Kluczowe odkrycia
searchViaExamożna wywołać bezpośrednio z rozpoznanym tekstem — pomijamyhandleSubmitiFormData, co upraszcza auto-submit.Micto dostępna ikona wlucide-react(ta sama biblioteka co istniejące ikony).- Input ma
pl-9dla lewej ikony;pr-9wystarczy dla prawej ikony mikrofonu. busyguard (searchState === 'loading' || 'searching') powinien objąć też'recording'.
Czego NIE robimy
- Nie dodajemy podglądu rozpoznawanego tekstu w czasie rzeczywistym (
interimResults: false). - Nie dodajemy wake-word / ciągłego nasłuchiwania — jednorazowe naciśnięcie przycisku.
- Nie zmieniamy backendu, env vars, schematu ani
search-via-exa-action.ts. - Nie dodajemy animacji pulsowania / waveform — zwykła zmiana koloru ikony wystarczy.
Podejście do implementacji
Dodajemy dwa nowe stany do SearchState, stałą isSpeechSupported i funkcję startVoiceSearch. Mikrofon jest absolutnie pozycjonowany w prawym rogu inputa. Na wynik STT wywołujemy searchViaExa bezpośrednio, pomijając formularz.
Krytyczne szczegóły implementacji
window.SpeechRecognitionnie istnieje podczas SSR — sprawdzenie musi być wewnątrz'use client'komponentu, ale i tak wuseStateinitializerze lubuseEffect, żeby nie crashowało na serwerze. Bezpieczna forma:typeof window !== 'undefined' && !!(window.SpeechRecognition || (window as any).webkitSpeechRecognition).recognition.lang = 'pl-PL'— bez tego Chrome używa języka przeglądarki, który może być angielski.- Import
Miczlucide-reacti pierwsze użycie muszą być w jednym Edit (global rules: imports + first usage in one Edit).
Faza 1: Mic button + voice recognition
Przegląd
Rozszerzamy AddRecipeForm o przycisk mikrofonu i logikę Web Speech API. Wszystkie zmiany w jednym pliku.
Wymagane zmiany
1. src/app/components/add-recipe-form.tsx
Plik: src/app/components/add-recipe-form.tsx
Cel: Dodać ikonę mikrofonu do inputa search taba; po rozpoznaniu mowy bezpośrednio wywołać searchViaExa i zaktualizować stan formularza.
Kontrakt:
SearchStaterozszerzony o'recording'i'mic_error'.isSpeechSupported: boolean— computed viatypeof window !== 'undefined' && !!(window.SpeechRecognition || (window as any).webkitSpeechRecognition). Determinuje czy przycisk mic się renderuje.busyconstant obejmuje nowe stany:searchState === 'loading' || searchState === 'searching' || searchState === 'recording'.startVoiceSearch()— funkcja tworzącaSpeechRecognitionzlang: 'pl-PL',interimResults: false,maxAlternatives: 1. WywołujesetSearchState('recording')irecognition.start(). Handlery:onresult: wyciągatranscript = event.results[0][0].transcript, wywołujesetSearchState('searching')+searchViaExa(transcript)(ten sam pattern co whandleSubmit), ustawiasetExaResultsisetSearchState('results'/'error').onerror:setSearchState('mic_error').onend: jeślisearchStatenadal'recording', reset do'idle'.
- Przycisk mic:
<button type="button">z<Mic>icon, absolutna pozycjaright-3 top-1/2 -translate-y-1/2, widoczny tylko gdyisSpeechSupported && activeTab === 'search'. Podczas'recording': kolortext-orange-500(vs normalnytext-gray-400).onClick: startVoiceSearch,disabled: busy. - Input className: gdy
activeTab === 'search' && isSpeechSupported, dodajpr-9(obok istniejącegopl-9). - Komunikat błędu mic: gdy
searchState === 'mic_error', renderuj<p role="alert" className="mt-1.5 text-sm text-red-600">Brak dostępu do mikrofonu — wpisz ręcznie.</p>— identyczny styl jak błąd Exa w linii 188–192. - Import
Micfromlucide-reactw tym samym Edit co pierwsze użycie (per global rule).
Kryteria sukcesu
Weryfikacja automatyczna
pnpm typecheckprzechodzi — nowe stany i typy poprawnie wytypowane.pnpm lintprzechodzi —Miczaimportowany i użyty w tym samym Edit.
Weryfikacja ręczna
- Chrome / Edge: ikona Mic widoczna w tabie “Wyszukaj przepis”, niewidoczna w “Dodaj przez link”.
- Kliknięcie mic → przeglądarka prosi o uprawnienia → po “Zezwól” → nasłuchiwanie → wyniki Exa pojawiają się automatycznie.
- Odmowa uprawnień → czerwony komunikat “Brak dostępu do mikrofonu — wpisz ręcznie.”
- Firefox: brak ikony mic (graceful degradation), wyszukiwanie tekstowe działa normalnie.
- Tab “Dodaj przez link” — URL flow bez regresji.
- Wyszukiwanie tekstowe (bez głosu) — bez regresji.
Strategia testowania
Testy ręczne
- Chrome mobile (Android/iOS) — mic + Exa flow end-to-end.
- Safari iOS —
webkitSpeechRecognition, Exa flow end-to-end. - Firefox — brak przycisku mic; wyszukiwanie tekstowe działa.
- Odmowa mikrofonu w Chrome — czerwony komunikat.
- URL wklejony w “Dodaj przez link” — brak regresji.
Uwagi dotyczące migracji
Brak — zmiana czysto addytywna, zero modyfikacji schematu lub serwera.
Referencje
- Komponent:
src/app/components/add-recipe-form.tsx - Server action:
src/app/(authenticated)/recipes/search-via-exa-action.ts
Postęp
Konwencja:
- [ ]oczekujące,- [x]wykonane. Dodaj— <commit sha>, gdy krok zostanie zrealizowany.
Faza 1: Mic button + voice recognition
Automatyczne
- 1.1
pnpm typecheckprzechodzi - 1.2
pnpm lintprzechodzi
Ręczne
- 1.3 Chrome: mic widoczny w search tabie, klik → Exa wyniki
- 1.4 Odmowa uprawnień → czerwony komunikat
- 1.5 Firefox: brak przycisku mic, wyszukiwanie tekstowe działa
- 1.6 Brak regresji w URL flow i wyszukiwaniu tekstowym