feat(app): Aufnahme verwerfen — Muelleimer neben dem Aufnahmeknopf

Bisher hiess "Stopp" immer "senden"; wer es sich anders ueberlegt hatte,
konnte die Aufnahme nicht zuruecknehmen.

- App: waehrend der Aufnahme 🗑 links neben dem roten Knopf. Verwirft ueber
  ariaStopRecording('user-discard') → cancelStreamingRecording; das leere
  Endpoint raeumt die Sprechblase ab, zurueck aufs Wake-Word.
- Fund dabei: "cancel" schickte der STT-Bridge nur stt_stream_end — die
  transkribierte trotzdem und der Text ging an ARIA (fiel nur nicht auf,
  weil cancel bisher bei Stille kam). Jetzt:
  - Whisper- und Voxtral-Bridge: stream_end mit reason "cancel:*" →
    discard, kein Transcribe, leerer Text (reason "cancelled").
  - aria-bridge merkt sich verworfene requestIds (2 min) und wirft einen
    trotzdem ankommenden Text weg — Schutz auch bei alter STT-Bridge.

Getestet: Whisper-Bridge mit nachgebautem Runner (Stopp → Text, Muelleimer
→ kein Transcribe, leer). App: TypeScript ohne Fehler; auf dem Handy noch
nicht ausprobiert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-31 20:05:35 +01:00
co-authored by Claude Opus 5.5
parent 91619d9307
commit 9529bb0b16
5 changed files with 81 additions and 5 deletions
+39
View File
@@ -35,6 +35,8 @@ interface VoiceButtonProps {
onTapStart: () => Promise<boolean>;
/** User hat nochmal getippt — ChatScreen soll stopStreamingRecording rufen. */
onTapStop: () => Promise<void>;
/** Muelleimer waehrend der Aufnahme — verwerfen, NICHTS an ARIA senden. */
onTapCancel?: () => Promise<void>;
/** Button deaktivieren */
disabled?: boolean;
/** Wake-Word-Modus aktiv (zeigt gruenen Indikator-Dot) */
@@ -46,6 +48,7 @@ interface VoiceButtonProps {
const VoiceButton: React.FC<VoiceButtonProps> = ({
onTapStart,
onTapStop,
onTapCancel,
disabled = false,
wakeWordActive = false,
}) => {
@@ -136,6 +139,16 @@ const VoiceButton: React.FC<VoiceButtonProps> = ({
}
}, [disabled, onTapStart, onTapStop]);
const handleCancel = useCallback(async () => {
if (!onTapCancel || tapBusy.current) return;
tapBusy.current = true;
try {
if (audioService.getRecordingState() === 'recording') await onTapCancel();
} finally {
tapBusy.current = false;
}
}, [onTapCancel]);
const formatDuration = (ms: number): string => {
const seconds = Math.floor(ms / 1000);
const tenths = Math.floor((ms % 1000) / 100);
@@ -154,6 +167,16 @@ const VoiceButton: React.FC<VoiceButtonProps> = ({
{wakeWordActive && !isRecording && (
<View style={styles.wakeWordDot} />
)}
{isRecording && onTapCancel && (
<TouchableOpacity
onPress={handleCancel}
accessibilityLabel="Aufnahme verwerfen"
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
style={styles.cancelButton}
>
<Text style={styles.cancelIcon}>🗑</Text>
</TouchableOpacity>
)}
<Animated.View
style={[
styles.buttonOuter,
@@ -197,6 +220,22 @@ const styles = StyleSheet.create({
backgroundColor: '#34C759',
zIndex: 10,
},
// Muelleimer links neben dem Aufnahmeknopf — nur waehrend der Aufnahme
cancelButton: {
position: 'absolute',
left: -46,
top: 14,
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: 'rgba(60, 60, 70, 0.9)',
alignItems: 'center',
justifyContent: 'center',
zIndex: 10,
},
cancelIcon: {
fontSize: 18,
},
buttonOuter: {
width: 64,
height: 64,
+10
View File
@@ -2624,6 +2624,15 @@ const ChatScreen: React.FC = () => {
}
}, []);
// Muelleimer waehrend der Aufnahme: verwerfen. cancelStreamingRecording
// schickt stt_stream_end mit reason "cancel:*" — die STT-Bridge transkribiert
// dann nicht, die aria-bridge wirft einen spaeten Text weg, und das leere
// Endpoint raeumt die Sprechblase ab. Danach zurueck aufs Wake-Word.
const handleVoiceButtonCancel = useCallback(async (): Promise<void> => {
console.log('[Chat] Aufnahme verworfen (Muelleimer)');
await ariaStopRecording('user-discard');
}, [ariaStopRecording]);
// Datei auswaehlen → zur Pending-Liste hinzufuegen
const handleFileSelected = useCallback(async (file: FileData) => {
setShowFileUpload(false);
@@ -3796,6 +3805,7 @@ const ChatScreen: React.FC = () => {
<VoiceButton
onTapStart={handleVoiceButtonStart}
onTapStop={handleVoiceButtonStop}
onTapCancel={handleVoiceButtonCancel}
disabled={connectionState !== 'connected'}
wakeWordActive={wakeWordActive}
/>