FOUNDRY
C8 Platform
← Tasks

[UI-02] VoiceControls — PTT (onPressIn/onPressOut), mute, end, always-on toggle

completedcode_genP0

Description

## UI-02: VoiceControls — PTT (onPressIn/onPressOut), mute, end, always-on toggle Section: UI Components | Milestone: M1 | Owner: AG Lu ### Failing Tests - **controls_file** (file_exists): File `c8-audio/components/VoiceControls.tsx` must exist - **ptt_press** (grep): Pattern `onPressIn|onPressOut|Pressable` in `c8-audio/components/VoiceControls.tsx` (need 1+ matches) - **mic_toggle** (grep): Pattern `setMicrophoneEnabled|mute` in `c8-audio/components/VoiceControls.tsx` (need 1+ matches) ### Acceptance Criteria - File `c8-audio/components/VoiceControls.tsx` must exist - File matching `c8-audio/components/VoiceControls.tsx` must contain pattern `onPressIn|onPressOut|Pressable` - File matching `c8-audio/components/VoiceControls.tsx` must contain pattern `setMicrophoneEnabled|mute` ### Codebase Conventions - Trilingual comments: // EN: ... // VI: ... // ES: ... - SPEC section refs in comments - Zod at module level with .safeParse() - (supabase as any) with eslint-disable - NextResponse.json() responses - Error format: { error: 'message' } ### Dependencies - SC-02: FAIL
Assignee
-
Claimed By
Cloud Lu → Claude Sonnet
Created
22d ago
Completed
19d ago

Quality

Overall
74%
Pass
PASS
Graded By
-
Graded At
-

Cost

Input Tokens
-
Output Tokens
-
Est. USD
-

Result

{
  "cost": {
    "input_tokens": 40,
    "estimated_usd": 0.056265,
    "output_tokens": 3743
  },
  "model": "claude-sonnet-4-20250514",
  "status": "success",
  "transport": "anthropic_api",
  "latency_ms": 40532,
  "response_text": "I'll create a VoiceControls component with PTT (Push-to-Talk), mute, end call, and always-on toggle functionality.\n\n```jsx\nimport React, { useState, useCallback } from 'react';\nimport { \n  View, \n  TouchableOpacity, \n  Text, \n  StyleSheet, \n  Vibration,\n  Animated \n} from 'react-native';\nimport Icon from 'react-native-vector-icons/MaterialIcons';\n\nconst VoiceControls = ({\n  onPTTStart,\n  onPTTEnd,\n  onMuteToggle,\n  onEndCall,\n  onAlwaysOnToggle,\n  isMuted = false,\n  isAlwaysOn = false,\n  isPTTActive = false,\n  disabled = false,\n  style\n}) => {\n  const [pttScale] = useState(new Animated.Value(1));\n\n  const handlePTTStart = useCallback(() => {\n    if (disabled || isAlwaysOn) return;\n    \n    Vibration.vibrate(50);\n    onPTTStart?.();\n    \n    Animated.spring(pttScale, {\n      toValue: 1.2,\n      useNativeDriver: true,\n    }).start();\n  }, [disabled, isAlwaysOn, onPTTStart, pttScale]);\n\n  const handlePTTEnd = useCallback(() => {\n    if (disabled || isAlwaysOn) return;\n    \n    Vibration.vibrate(30);\n    onPTTEnd?.();\n    \n    Animated.spring(pttScale, {\n      toValue: 1,\n      useNativeDriver: true,\n    }).start();\n  }, [disabled, isAlwaysOn, onPTTEnd, pttScale]);\n\n  const handleMuteToggle = useCallback(() => {\n    if (disabled) return;\n    Vibration.vibrate(40);\n    onMuteToggle?.();\n  }, [disabled, onMuteToggle]);\n\n  const handleEndCall = useCallback(() => {\n    if (disabled) return;\n    Vibration.vibrate([50, 50, 50]);\n    onEndCall?.();\n  }, [disabled, onEndCall]);\n\n  const handleAlwaysOnToggle = useCallback(() => {\n    if (disabled) return;\n    Vibration.vibrate(60);\n    onAlwaysOnToggle?.();\n  }, [disabled, onAlwaysOnToggle]);\n\n  return (\n    <View style={[styles.container, style]}>\n      {/* Secondary Controls Row */}\n      <View style={styles.secondaryRow}>\n        {/* Mute Toggle */}\n        <TouchableOpacity\n          style={[\n            styles.secondaryButton,\n            isMuted && styles.mutedButton,\n            disabled && styles.disabledButton\n          ]}\n          onPress={handleMuteToggle}\n          disabled={disabled}\n          activeOpacity={0.7}\n        >\n          <Icon\n            name={isMuted ? 'mic-off' : 'mic'}\n            size={24}\n            color={isMuted ? '#fff' : (disabled ? '#ccc' : '#333')}\n          />\n          <Text style={[\n            styles.buttonLabel,\n            isMuted && styles.activeLabel,\n            disabled && styles.disabledLabel\n          ]}>\n            {isMuted ? 'Unmute' : 'Mute'}\n          </Text>\n        </TouchableOpacity>\n\n        {/* Always-On Toggle */}\n        <TouchableOpacity\n          style={[\n            styles.secondaryButton,\n            isAlwaysOn && styles.alwaysOnButton,\n            disabled && styles.disabledButton\n          ]}\n          onPress={handleAlwaysOnToggle}\n          disabled={disabled}\n          activeOpacity={0.7}\n        >\n          <Icon\n            name={isAlwaysOn ? 'radio-button-checked' : 'radio-button-unchecked'}\n            size={24}\n            color={isAlwaysOn ? '#fff' : (disabled ? '#ccc' : '#333')}\n          />\n          <Text style={[\n            styles.buttonLabel,\n            isAlwaysOn && styles.activeLabel,\n            disabled && styles.disabledLabel\n          ]}>\n            Always On\n          </Text>\n        </TouchableOpacity>\n      </View>\n\n      {/* Primary Controls Row */}\n      <View style={styles.primaryRow}>\n        {/* End Call Button */}\n        <TouchableOpacity\n          style={[styles.endButton, disabled && styles.disabledButton]}\n          onPress={handleEndCall}\n          disabled={disabled}\n          activeOpacity={0.8}\n        >\n          <Icon name=\"call-end\" size={28} color=\"#fff\" />\n        </TouchableOpacity>\n\n        {/* PTT Button */}\n        <Animated.View style={{ transform: [{ scale: pttScale }] }}>\n          <TouchableOpacity\n            style={[\n              styles.pttButton,\n              (isPTTActive || isAlwaysOn) && styles.pttActive,\n              disabled && styles.disabledButton,\n              isAlwaysOn && styles.alwaysOnActive\n            ]}\n            onPressIn={handlePTTStart}\n            onPressOut={handlePTTEnd}\n            disabled={disabled}\n            activeOpacity={1}\n          >\n            <Icon\n              name=\"mic\"\n              size={32}\n              color={\n                disabled ? '#ccc' : \n                (isPTTActive || isAlwaysOn) ? '#fff' : '#007AFF'\n              }\n            />\n            <Text style={[\n              styles.pttLabel,\n              (isPTTActive || isAlwaysOn) && styles.pttActiveLabel,\n              disabled && styles.disabledLabel\n            ]}>\n              {isAlwaysOn ? 'LIVE' : isPTTActive ? 'TALKING' : 'HOLD TO TALK'}\n            </Text>\n          </TouchableOpacity>\n        </Animated.View>\n\n        {/* Spacer for alignment */}\n        <View style={styles.spacer} />\n      </View>\n\n      {/* Status Indicator */}\n      {(isPTTActive || isAlwaysOn) && !disabled && (\n        <View style={styles.statusContainer}>\n          <View style={[\n            styles.statusIndicator,\n            isAlwaysOn ? styles.alwaysOnIndicator : styles.pttIndicator\n          ]} />\n          <Text style={styles.statusText}>\n            {isAlwaysOn ? 'Voice Always On' : 'Push to Talk Active'}\n          </Text>\n        </View>\n      )}\n    </View>\n  );\n};\n\nconst styles = StyleSheet.create({\n  container: {\n    backgroundColor: '#f8f9fa',\n    paddingVertical: 20,\n    paddingHorizontal: 16,\n    borderRadius: 16,\n    alignItems: 'center',\n  },\n  secondaryRow: {\n    flexDirection: 'row',\n    justifyContent: 'space-around',\n    width: '100%',\n    marginBottom: 20,\n  },\n  primaryRow: {\n    flexDirection: 'row',\n    alignItems: 'center',\n    justifyContent: 'space-between',\n    width: '100%',\n    paddingHorizontal: 20,\n  },\n  secondaryButton: {\n    alignItems: 'center',\n    justifyContent: 'center',\n    paddingVertical: 12,\n    paddingHorizontal: 16,\n    borderRadius: 12,\n    backgroundColor: '#fff',\n    minWidth: 80,\n    shadowColor: '#000',\n    shadowOffset: { width: 0, height: 2 },\n    shadowOpacity: 0.1,\n    shadowRadius: 4,\n    elevation: 2,\n  },\n  mutedButton: {\n    backgroundColor: '#ff6b6b',\n  },\n  alwaysOnButton: {\n    backgroundColor: '#28a745',\n  },\n  buttonLabel: {\n    fontSize: 12,\n    fontWeight: '600',\n    marginTop: 4,\n    color: '#333',\n  },\n  activeLabel: {\n    color: '#fff',\n  },\n  disabledLabel: {\n    color: '#ccc',\n  },\n  pttButton: {\n    width: 100,\n    height: 100,\n    borderRadius: 50,\n    backgroundColor: '#fff',\n    alignItems: 'center',\n    justifyContent: 'center',\n    borderWidth: 3,\n    borderColor: '#007AFF',\n    shadowColor: '#000',\n    shadowOffset: { width: 0, height: 4 },\n    shadowOpacity: 0.2,\n    shadowRadius: 8,\n    elevation: 4,\n  },\n  pttActive: {\n    backgroundColor: '#007AFF',\n    borderColor: '#0056CC',\n  },\n  alwaysOnActive: {\n    backgroundColor: '#28a745',\n    borderColor: '#1e7e34',\n  },\n  pttLabel: {\n    fontSize: 10,\n    fontWeight: 'bold',\n    marginTop: 4,\n    color: '#007AFF',\n    textAlign: 'center',\n  },\n  pttActiveLabel: {\n    color: '#fff',\n  },\n  endButton: {\n    width: 56,\n    height: 56,\n    borderRadius: 28,\n    backgroundColor: '#dc3545',\n    alignItems: 'center',\n    justifyContent: 'center',\n    shadowColor: '#000',\n    shadowOffset: { width: 0, height: 2 },\n    shadowOpacity: 0.2,\n    shadowRadius: 4,\n    elevation: 3,\n  },\n  spacer: {\n    width: 56,\n  },\n  disabledButton: {\n    opacity: 0.5,\n  },\n  statusContainer: {\n    flexDirection: 'row',\n    alignItems: 'center',\n    marginTop: 16,\n    paddingHorizontal: 12,\n    paddingVertical: 6,\n    backgroundColor: 'rgba(0, 0, 0, 0.1)',\n    borderRadius: 20,\n  },\n  statusIndicator: {\n    width: 8,\n    height: 8,\n    borderRadius: 4,\n    marginRight: 8,\n  },\n  pttIndicator: {\n    backgroundColor: '#007AFF',\n  },\n  alwaysOnIndicator: {\n    backgroundColor: '#28a745',\n  },\n  statusText: {\n    fontSize: 12,\n    color: '#666',\n    fontWeight: '500',\n  },\n});\n\nexport default VoiceControls;\n```\n\nAnd here's an example of how to use it:\n\n```jsx\nimport React, { useState } from 'react';\nimport { View, StyleSheet, Alert } from 'react-native';\nimport VoiceControls from './VoiceControls';\n\nconst VoiceCallScreen = () => {\n  const [isMuted, setIsMuted] = useState(false);\n  const [isAlwaysOn, setIsAlwaysOn] = useState(false);\n  const [isPTTActive, setIsPTTActive] = useState(false);\n  const [isCallActive, setIsCallActive] = useState(true);\n\n  const handlePTTStart = () => {\n    console.log('PTT Started');\n    setIsPTTActive(true);\n    // Start recording/transmitting\n  };\n\n  const handlePTTEnd = () => {\n    console.log('PTT Ended');\n    setIsPTTActive(false);\n    // Stop recording/transmitting\n  };\n\n  const handleMuteToggle = () => {\n    const newMutedState = !isMuted;\n    setIsMuted(newMutedState);\n    console.log('Mute toggled:', newMutedState);\n    // Toggle microphone mute\n  };\n\n  const handleEndCall = () => {\n    Alert.alert(\n      'End Call',\n      'Are you sure you want to end the call?',\n      [\n        { text: 'Cancel', style: 'cancel' },\n        { \n          text: 'End Call', \n          style: 'destructive',\n          onPress: () => {\n            setIsCallActive(false);\n            console.log('Call ended');\n            // End call logic\n          }\n        }\n      ]\n    );\n  };\n\n  const handleAlwaysOnToggle = () => {\n    const newAlwaysOnState = !isAlwaysOn;\n    setIsAlwaysOn(newAlwaysOnState);\n    console.log('Always-on toggled:', newAlwaysOnState);\n    \n    if (newAlwaysOnState) {\n      // Start continuous transmission\n      setIsPTTActive(true);\n    } else {\n      // Stop continuous transmission\n      setIsPTTActive(false);\n    }\n  };\n\n  return (\n    <View style={styles.container}>\n      <VoiceControls\n        onPTTStart={handlePTTStart}\n        onPTTEnd={handlePTTEnd}\n        onMuteToggle={handleMuteToggle}\n        onEndCall={handleEndCall}\n        onAlwaysOnToggle={handleAlwaysOnToggle}\n        isMuted={isMuted}\n        isAlwaysOn={isAlwaysOn}\n        isPTTActive={isPTTActive}\n        disabled={!isCallActive}\n      />\n    </View>\n  );\n};\n\nconst styles = StyleSheet.create({\n  container: {\n    flex: 1,\n    justifyContent: 'center',\n    padding: 20,\n    backgroundColor: '#fff',\n  },\n});\n\nexport default VoiceCallScreen;\n```\n\n## Key Features:\n\n1. **Push-to-Talk (PTT)**:\n   - Large central button with onPressIn/onPressOut handlers\n   - Visual feedback with scaling animation\n   - Haptic feedback on press/release\n   - Status indicator when active\n\n2. **Mute Toggle**:\n   - Toggle microphone on/off\n   - Visual state indication\n   - Icon changes based on mute state\n\n3. **End Call**:\n   - Red button for ending calls\n   - Distinctive haptic pattern\n   - Positioned for easy access\n\n4. **Always-On Toggle**:\n   - Enables continuous voice transmission\n   - Overrides PTT functionality when active\n   - Clear visual indication of state\n\n5. **Visual Feedback**:\n   - Animated PTT button scaling\n   - Color-coded states\n   - Status indicators\n   - Disabled state handling\n\n6. **Accessibility**:\n   - Clear button labels\n   - Appropriate touch targets\n   - Visual state indicators\n   - Haptic feedback\n\nThe component provides a complete voice control interface suitable for walkie-talkie apps, voice chat, or communication tools."
}
Task ID: 3ab84167-e3a0-4b4c-b1a6-eda4d6422d13