Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 159 additions & 14 deletions .docs/TECHNICAL_DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -833,25 +833,31 @@ interface VoiceStatusIndicatorProps {
```typescript
interface VoiceActivationBannerProps {
visible: boolean;
isListening?: boolean;
}
```

**Design:**
- Light green background (#E8F5E9)
- Green bottom border (#4CAF50)
- Message: "🎤 Voice Activation On - Say 'Hey Shoppy' to start"
- Dynamic message based on listening state:
- Not listening: "🎤 Voice Activation On - Say 'Hey Shoppy' to start"
- Listening: "🎤 Voice Activation On - Listening..."
- Conditionally rendered (null when hidden)
- Full-width banner at top of screen

**Usage Example:**
```typescript
<VoiceActivationBanner visible={voiceActivationEnabled} />
<VoiceActivationBanner
visible={voiceActivationEnabled}
isListening={wakeWord?.isListening}
/>
```

**"Hey Shoppy" Wake Word:**
- Chosen for restaurant chef use case (hands-free operation)
- Alternative to "Hey Chef" for better branding
- Will use expo-speech-recognition in Task 4
- Implemented via WakeWordService
- Mentioned consistently across all voice UI

---
Expand All @@ -862,25 +868,97 @@ interface VoiceActivationBannerProps {

**Features:**
- Voice Features section header
- **Toggle 1:** Enable Voice Activation
- **Toggle 1:** Enable Voice Activation (FUNCTIONAL)
- Label: "Say 'Hey Shoppy' to activate"
- Message: "(Coming in Task 4)"
- Currently disabled, shows info alert when pressed
- When WakeWordContext available: Toggle enables/disables wake word detection
- Shows "Enabled"/"Disabled" status
- Falls back to "Coming in Task 4" message if context unavailable

- **Toggle 2:** Voice Button on Lists
- Label: "Tap microphone button"
- Message: "(Coming Soon)"
- Currently disabled, shows info alert when pressed

**Alert Messages (Phase 6):**
- Voice Activation Alert: "Voice activation with 'Hey Shoppy' wake word will be available when we integrate OpenAI Whisper and GPT-4."
- Voice Button Alert: "Voice button functionality will be available in the next update. We're building the voice recognition features!"
**Integration with WakeWordContext:**
- Toggle controls `wakeWord.setEnabled()`
- Value reflects `wakeWord.enabled` state
- Status shows dynamically based on enabled state

---

**6. WakeWordService** (`services/wake-word-service.ts`)

**Purpose:** Core service for "Hey Shoppy" wake word detection

**API:**
```typescript
export type WakeWordStatus = 'idle' | 'listening' | 'detected';

export interface WakeWordServiceOptions {
onWakeWordDetected: (remainingTranscript: string) => void;
onStatusChange: (status: WakeWordStatus) => void;
}

export class WakeWordService {
getWakePhrase(): string; // Returns "hey shoppy"
getStatus(): WakeWordStatus;
isListening(): boolean;
isEnabled(): boolean;
setEnabled(enabled: boolean): void;
start(): void; // Start listening
stop(): void; // Stop listening
processTranscript(transcript: string): boolean; // Check for wake word
resetAfterDetection(): void; // Reset to listening state
}
```

**Wake Word Detection:**
- Detects "hey shoppy" (case-insensitive)
- Extracts command text after wake phrase
- Example: "Hey Shoppy add milk" → callback receives "add milk"

---

**7. useWakeWord Hook** (`hooks/use-wake-word.ts`)

**Purpose:** React hook wrapper for WakeWordService

**API:**
```typescript
interface UseWakeWordOptions {
onWakeWordDetected?: (command: string) => void;
}

interface UseWakeWordResult {
status: WakeWordStatus;
isListening: boolean;
enabled: boolean;
wakePhrase: string;
detectedCommand: string | null;
startListening: () => void;
stopListening: () => void;
setEnabled: (enabled: boolean) => void;
processTranscript: (transcript: string) => boolean;
resetDetection: () => void;
}
```

---

**8. WakeWordContext** (`contexts/wake-word-context.tsx`)

**Purpose:** App-wide state sharing for wake word functionality

**Usage:**
```typescript
// In root layout
<WakeWordProvider onWakeWordDetected={handleCommand}>
<App />
</WakeWordProvider>

**Integration (Task 4):**
- Toggles become functional
- Enable/disable voice activation
- Control wake word detection
- Configure voice sensitivity
// In any child component
const { isListening, startListening, processTranscript } = useWakeWordContext();
```

---

Expand Down Expand Up @@ -2136,6 +2214,73 @@ CORS_ORIGINS=http://localhost:8081,http://localhost:19000,http://localhost:19006

## Changelog

### Version 0.5.0 - November 28, 2025 ("Hey Shoppy" Wake Word Detection)

**Added:**
- Wake word detection service for "Hey Shoppy" activation
- Custom `useWakeWord` hook for React integration
- `WakeWordContext` for app-wide state management
- Settings toggle now enables/disables wake word detection
- VoiceActivationBanner shows listening state
- VoiceStatusIndicator reflects wake word detection status

**Wake Word Feature:**
- Users can say "Hey Shoppy" to start voice command listening
- Works in addition to pressing the microphone button
- Settings toggle in Voice Features section controls activation
- Banner shows "Listening..." when actively listening for wake word
- Green indicator when ready, red when listening

**Architecture:**
- `WakeWordService` - Core service for wake word detection
- Processes transcripts and detects "hey shoppy" phrase
- Supports enable/disable functionality
- Status states: idle, listening, detected
- Extracts command text after wake phrase

- `useWakeWord` hook - React hook wrapper
- Manages service lifecycle
- Provides state: status, isListening, enabled, detectedCommand
- Methods: startListening, stopListening, setEnabled, processTranscript

- `WakeWordProvider` - Context provider
- App-wide state sharing
- Integrated in root layout
- Optional onWakeWordDetected callback

**Test Coverage:**
- WakeWordService: 29 tests
- useWakeWord hook: 16 tests
- WakeWordContext: 14 tests
- Settings integration: 6 tests
- VoiceActivationBanner: 11 tests
- Total: 194 tests, all passing

**Usage:**
```typescript
// In any component within WakeWordProvider
const { isListening, startListening, processTranscript } = useWakeWordContext();

// Start listening for wake word
startListening();

// Process speech transcript (from speech recognition)
processTranscript('hey shoppy add milk');
// Returns true, calls onWakeWordDetected with 'add milk'
```

**Settings Screen:**
- Voice Activation toggle now functional when context available
- Shows "Enabled"/"Disabled" status
- Toggle controls wake word detection globally

**Next Steps:**
- Integrate with expo-speech-recognition for actual speech-to-text
- Connect detected commands to shopping list actions
- Add voice feedback using text-to-speech

---

### Version 0.4.0 - November 23, 2025 (Voice UI Stubs - Phase 6)

**Added:**
Expand Down
56 changes: 44 additions & 12 deletions shopping-list/app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,30 @@ import { useShoppingLists, useCreateShoppingList, useDeleteShoppingList } from '
import { ThemedView } from '@/components/themed-view';
import { ThemedText } from '@/components/themed-text';
import { ConfirmationModal } from '@/components/ui/confirmation-modal';
import { VoiceStatusIndicator } from '@/components/VoiceStatusIndicator';
import { VoiceStatusIndicator, VoiceStatus } from '@/components/VoiceStatusIndicator';
import { VoiceActivationBanner } from '@/components/VoiceActivationBanner';
import { Colors } from '@/constants/theme';
import { useColorScheme } from '@/hooks/use-color-scheme';
import { useWakeWordContext } from '@/contexts/wake-word-context';
import type { ShoppingListWithCount } from '@/types/api';

// Try to get wake word context, return null if not available
function useTryWakeWordContext() {
try {
return useWakeWordContext();
} catch {
return null;
}
}

Comment on lines 30 to +40
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a duplicate of the same helper function in settings.tsx and list-detail.tsx. Extract to a shared utility to maintain DRY principle.

Suggested change
// Try to get wake word context, return null if not available
function useTryWakeWordContext() {
try {
return useWakeWordContext();
} catch {
return null;
}
}
import { useTryWakeWordContext } from '@/hooks/use-try-wake-word-context';

Copilot uses AI. Check for mistakes.
export default function ShoppingListsScreen() {
const router = useRouter();
const { data: lists, isLoading, isError, error, refetch } = useShoppingLists();
const createMutation = useCreateShoppingList();
const deleteMutation = useDeleteShoppingList();
const colorScheme = useColorScheme();
const colors = Colors[colorScheme ?? 'light'];
const wakeWord = useTryWakeWordContext();

// Create modal state
const [isCreateModalVisible, setIsCreateModalVisible] = useState(false);
Expand All @@ -46,16 +57,37 @@ export default function ShoppingListsScreen() {
const [listToDelete, setListToDelete] = useState<{ id: string; name: string } | null>(null);
const [deleteError, setDeleteError] = useState('');

// Voice activation state (stub for Phase 6)
const [voiceActivationEnabled] = useState(false);
// Voice activation state - use wake word context if available
const voiceActivationEnabled = wakeWord?.enabled ?? false;

// Get voice status based on wake word context
const getVoiceStatus = (): VoiceStatus => {
if (!wakeWord) return 'coming-soon';
switch (wakeWord.status) {
case 'listening':
return 'listening';
case 'detected':
return 'processing';
default:
return 'ready';
}
};

// Handle voice button press
const handleVoicePress = () => {
Alert.alert(
'Voice Commands Coming Soon',
'Voice-powered list creation will be available in the next update. Say "Hey Shoppy" to get started!',
[{ text: 'OK' }]
);
if (wakeWord) {
if (wakeWord.isListening) {
wakeWord.stopListening();
} else {
wakeWord.startListening();
}
} else {
Alert.alert(
'Voice Commands Coming Soon',
'Voice-powered list creation will be available in the next update. Say "Hey Shoppy" to get started!',
[{ text: 'OK' }]
);
}
};

// Handle create list
Expand Down Expand Up @@ -183,13 +215,13 @@ export default function ShoppingListsScreen() {
if (!lists || lists.length === 0) {
return (
<ThemedView style={styles.container}>
<VoiceActivationBanner visible={voiceActivationEnabled} />
<VoiceActivationBanner visible={voiceActivationEnabled} isListening={wakeWord?.isListening} />
<View style={styles.header}>
<ThemedText type="title" style={styles.headerTitle}>
Shopping Lists
</ThemedText>
<View style={styles.headerActions}>
<VoiceStatusIndicator status="coming-soon" onPress={handleVoicePress} />
<VoiceStatusIndicator status={getVoiceStatus()} onPress={handleVoicePress} />
<TouchableOpacity
testID="create-list-button"
style={[styles.createButton, { backgroundColor: colors.tint }]}
Expand Down Expand Up @@ -249,13 +281,13 @@ export default function ShoppingListsScreen() {

return (
<ThemedView style={styles.container}>
<VoiceActivationBanner visible={voiceActivationEnabled} />
<VoiceActivationBanner visible={voiceActivationEnabled} isListening={wakeWord?.isListening} />
<View style={styles.header}>
<ThemedText type="title" style={styles.headerTitle}>
Shopping Lists
</ThemedText>
<View style={styles.headerActions}>
<VoiceStatusIndicator status="coming-soon" onPress={handleVoicePress} />
<VoiceStatusIndicator status={getVoiceStatus()} onPress={handleVoicePress} />
<TouchableOpacity
testID="create-list-button"
style={[styles.createButton, { backgroundColor: colors.tint }]}
Expand Down
Loading