Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
9bc90b8
feat: keyboard/edit UX — fix keyboard coverage, multiline title, expo…
End2EndAI Mar 27, 2026
819fa8f
feat: sort notes + default tag preference
End2EndAI Mar 27, 2026
115a03d
feat: default filter tag selector in settings
End2EndAI Mar 27, 2026
db341fd
feat: interactive acronym/term validation after transcription (#2)
End2EndAI Mar 27, 2026
6e1f64f
fix: resolve 3 bugs found in new features review
End2EndAI Mar 29, 2026
a5bbec1
fix: restore proper json_schema structured output for uncertain terms…
End2EndAI Mar 30, 2026
41eaabe
fix: inject validated terms into note reformat flow
End2EndAI Mar 30, 2026
66207a6
feat: add Saved Terms screen to view and manage validated acronyms
End2EndAI Mar 30, 2026
b7b00a4
feat: pass known terms to transcribe so already-validated acronyms ar…
End2EndAI Mar 30, 2026
ff8ee3e
fix: replace Recordings text button with icon to prevent header overflow
End2EndAI Apr 2, 2026
4b2a4ef
fix: header overflow, action items UX, sort placement
End2EndAI Apr 2, 2026
3dfc158
feat: drag-to-reorder notes with manual sort mode
End2EndAI Apr 2, 2026
d4412e5
feat: long-press copy, 1-tag AI limit, tag picker on text note creation
End2EndAI Apr 2, 2026
dcdd9f4
fix: crash when selecting manual sort due to undefined manualOrder
End2EndAI Apr 2, 2026
b45ab82
fix: prevent page refresh on drag, make manual order the default sort
End2EndAI Apr 2, 2026
0873a7e
feat: reorder mode toggle, fix pull-to-refresh conflict on drag
End2EndAI Apr 2, 2026
c440f16
fix: replace recordings icon with waveform to distinguish from reorde…
End2EndAI Apr 2, 2026
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
4 changes: 4 additions & 0 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ function RootLayoutNav() {
name="trash"
options={{ title: 'Trash', headerShown: false }}
/>
<Stack.Screen
name="acronyms"
options={{ title: 'Saved Terms', headerShown: false }}
/>
</Stack>
</>
);
Expand Down
272 changes: 272 additions & 0 deletions app/acronyms.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
import React, { useEffect, useState, useCallback } from 'react';
import {
View,
Text,
StyleSheet,
Pressable,
FlatList,
ActivityIndicator,
} from 'react-native';
import { useRouter } from 'expo-router';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Colors } from '../constants/colors';
import {
ValidatedTerm,
loadValidatedTerms,
deleteValidatedTerm,
deleteAllValidatedTerms,
} from '../services/validatedTerms';
import { showConfirm, showAlert } from '../utils/alert';

export default function AcronymsScreen() {
const router = useRouter();
const [terms, setTerms] = useState<ValidatedTerm[]>([]);
const [loading, setLoading] = useState(true);
const [deleting, setDeleting] = useState<string | null>(null);

const load = useCallback(async () => {
try {
const data = await loadValidatedTerms();
setTerms(data);
} catch {
showAlert('Error', 'Could not load saved terms.');
} finally {
setLoading(false);
}
}, []);

useEffect(() => {
load();
}, [load]);

const handleDelete = (term: ValidatedTerm) => {
showConfirm(
'Remove term',
`Remove "${term.original_term}" → "${term.corrected_term}"? It will no longer be used in future transcriptions.`,
async () => {
setDeleting(term.original_term);
try {
await deleteValidatedTerm(term.original_term);
setTerms((prev) => prev.filter((t) => t.original_term !== term.original_term));
} catch {
showAlert('Error', 'Could not remove the term.');
} finally {
setDeleting(null);
}
}
);
};

const handleClearAll = () => {
if (terms.length === 0) return;
showConfirm(
'Clear all terms',
'Remove all saved corrections? This cannot be undone.',
async () => {
try {
await deleteAllValidatedTerms();
setTerms([]);
} catch {
showAlert('Error', 'Could not clear terms.');
}
}
);
};

return (
<SafeAreaView style={styles.container}>
<View style={styles.topBar}>
<Pressable
onPress={() => router.back()}
style={({ pressed }) => [styles.backPressable, pressed && { opacity: 0.6 }]}
>
<Text style={styles.backText}>Back</Text>
</Pressable>
<Text style={styles.topTitle}>Saved Terms</Text>
{terms.length > 0 ? (
<Pressable
onPress={handleClearAll}
style={({ pressed }) => [styles.clearPressable, pressed && { opacity: 0.6 }]}
>
<Text style={styles.clearText}>Clear all</Text>
</Pressable>
) : (
<View style={{ width: 72 }} />
)}
</View>

{loading ? (
<View style={styles.centered}>
<ActivityIndicator color={Colors.primary} />
</View>
) : terms.length === 0 ? (
<View style={styles.centered}>
<Text style={styles.emptyIcon}>🔤</Text>
<Text style={styles.emptyTitle}>No saved corrections</Text>
<Text style={styles.emptyDesc}>
When the AI flags uncertain terms during transcription and you confirm the correct
spelling, those corrections are saved here and reused automatically in future notes.
</Text>
</View>
) : (
<FlatList
data={terms}
keyExtractor={(item) => item.original_term}
contentContainerStyle={styles.listContent}
ListHeaderComponent={
<Text style={styles.listHeader}>
{terms.length} saved {terms.length === 1 ? 'correction' : 'corrections'}
</Text>
}
renderItem={({ item }) => (
<View style={styles.termRow}>
<View style={styles.termTexts}>
<View style={styles.termPair}>
<Text style={styles.originalText}>{item.original_term}</Text>
<Text style={styles.arrow}>→</Text>
<Text style={styles.correctedText}>{item.corrected_term}</Text>
</View>
</View>
<Pressable
onPress={() => handleDelete(item)}
disabled={deleting === item.original_term}
style={({ pressed }) => [
styles.deleteButton,
pressed && { opacity: 0.6 },
deleting === item.original_term && { opacity: 0.4 },
]}
accessibilityLabel={`Remove correction for ${item.original_term}`}
>
<Text style={styles.deleteText}>Remove</Text>
</Pressable>
</View>
)}
ItemSeparatorComponent={() => <View style={styles.separator} />}
/>
)}
</SafeAreaView>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: Colors.background,
},
topBar: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 20,
paddingVertical: 14,
borderBottomWidth: 1,
borderBottomColor: Colors.borderLight,
},
backPressable: {
paddingVertical: 4,
minWidth: 56,
},
backText: {
color: Colors.primary,
fontSize: 16,
fontWeight: '600',
},
topTitle: {
fontSize: 17,
fontWeight: '600',
color: Colors.text,
},
clearPressable: {
paddingVertical: 4,
minWidth: 72,
alignItems: 'flex-end',
},
clearText: {
color: Colors.error,
fontSize: 15,
fontWeight: '500',
},
centered: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 40,
},
emptyIcon: {
fontSize: 48,
marginBottom: 16,
},
emptyTitle: {
fontSize: 18,
fontWeight: '700',
color: Colors.text,
marginBottom: 10,
textAlign: 'center',
},
emptyDesc: {
fontSize: 14,
color: Colors.textTertiary,
textAlign: 'center',
lineHeight: 22,
},
listContent: {
padding: 20,
paddingBottom: 48,
},
listHeader: {
fontSize: 13,
fontWeight: '600',
color: Colors.textTertiary,
textTransform: 'uppercase',
letterSpacing: 0.6,
marginBottom: 16,
},
termRow: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: Colors.surface,
borderRadius: 14,
paddingHorizontal: 16,
paddingVertical: 14,
borderWidth: 1,
borderColor: Colors.borderLight,
},
termTexts: {
flex: 1,
},
termPair: {
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'wrap',
gap: 8,
},
originalText: {
fontSize: 14,
color: Colors.textSecondary,
fontStyle: 'italic',
},
arrow: {
fontSize: 14,
color: Colors.textTertiary,
},
correctedText: {
fontSize: 15,
fontWeight: '600',
color: Colors.text,
},
deleteButton: {
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 8,
backgroundColor: Colors.errorLight,
marginLeft: 12,
},
deleteText: {
fontSize: 12,
color: Colors.error,
fontWeight: '600',
},
separator: {
height: 8,
},
});
Loading
Loading