feat: App-Chat-Suche mit Next/Prev + Diagnostic Sprachausgabe-Layout
App Chat-Suche umgebaut von Filter zu Highlight+Navigation
Vorher: searchQuery filtert die FlatList, zeigt nur Treffer.
Jetzt: Suche filtert NICHT mehr, alle Nachrichten bleiben sichtbar.
Treffer wird gelb (FFD60A) umrandet, FlatList scrollt automatisch
dorthin.
- Suchleiste: Input + Counter "N/M" + ▲ + ▼ + ✕
- ▲ / ▼ navigieren chronologisch durch alle Matches (zyklisch)
- searchMatchIds via useMemo, searchIndex separates State
- scrollToIndex mit viewPosition: 0.4 (Treffer landet im oberen Drittel)
- onScrollToIndexFailed Fallback nach 200ms (Layout noch nicht fertig)
Diagnostic Sprachausgabe-Layout
Export/Import-Buttons wandern aus dem Section-Header in den Details-Block
neben "Anwenden" (Stefan's Wunsch). Header zeigt nur noch den Titel.
File-Input bleibt versteckt im Section-Top, wird vom neuen Button-Block
unten ueber click() getriggert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -201,6 +201,7 @@ const ChatScreen: React.FC = () => {
|
||||
const [fullscreenImage, setFullscreenImage] = useState<string | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [searchVisible, setSearchVisible] = useState(false);
|
||||
const [searchIndex, setSearchIndex] = useState(0); // welcher Treffer aktiv ist
|
||||
const [pendingAttachments, setPendingAttachments] = useState<{file: any, isPhoto: boolean}[]>([]);
|
||||
const [agentActivity, setAgentActivity] = useState<{activity: string, tool: string}>({activity: 'idle', tool: ''});
|
||||
// Service-Status (Gamebox: F5-TTS / Whisper Lade-Status) + Banner-Sichtbarkeit
|
||||
@@ -892,6 +893,43 @@ const ChatScreen: React.FC = () => {
|
||||
// Inverted FlatList: neueste Nachrichten unten, kein manuelles Scrollen noetig
|
||||
const invertedMessages = useMemo(() => [...messages].reverse(), [messages]);
|
||||
|
||||
// Such-Treffer: alle Message-IDs die zur Query passen, in chronologischer
|
||||
// Reihenfolge (aelteste zuerst). Bei Query-Change resetten wir den Index.
|
||||
const searchMatchIds = useMemo(() => {
|
||||
const q = searchQuery.trim().toLowerCase();
|
||||
if (!q) return [] as string[];
|
||||
return messages
|
||||
.filter(m => (m.text || '').toLowerCase().includes(q))
|
||||
.map(m => m.id);
|
||||
}, [messages, searchQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
setSearchIndex(0);
|
||||
}, [searchQuery]);
|
||||
|
||||
// Bei Index-Wechsel zu der entsprechenden Bubble scrollen
|
||||
useEffect(() => {
|
||||
if (!searchMatchIds.length) return;
|
||||
const id = searchMatchIds[searchIndex];
|
||||
if (!id) return;
|
||||
// invertedMessages → index in der angezeigten Liste finden
|
||||
const idx = invertedMessages.findIndex(m => m.id === id);
|
||||
if (idx < 0 || !flatListRef.current) return;
|
||||
try {
|
||||
flatListRef.current.scrollToIndex({ index: idx, animated: true, viewPosition: 0.4 });
|
||||
} catch {}
|
||||
}, [searchIndex, searchMatchIds, invertedMessages]);
|
||||
|
||||
const activeSearchId = searchMatchIds[searchIndex] || '';
|
||||
const gotoSearchPrev = () => {
|
||||
if (!searchMatchIds.length) return;
|
||||
setSearchIndex(i => (i - 1 + searchMatchIds.length) % searchMatchIds.length);
|
||||
};
|
||||
const gotoSearchNext = () => {
|
||||
if (!searchMatchIds.length) return;
|
||||
setSearchIndex(i => (i + 1) % searchMatchIds.length);
|
||||
};
|
||||
|
||||
// GPS-Position holen (optional)
|
||||
const getCurrentLocation = useCallback((): Promise<{ lat: number; lon: number } | null> => {
|
||||
if (!gpsEnabled) {
|
||||
@@ -1143,12 +1181,16 @@ const ChatScreen: React.FC = () => {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
const isSearchHit = activeSearchId === item.id;
|
||||
const searchHighlightStyle = isSearchHit
|
||||
? { borderWidth: 2, borderColor: '#FFD60A' }
|
||||
: null;
|
||||
|
||||
// Spezial-Bubble: ARIA hat einen Skill erstellt
|
||||
if (item.skillCreated) {
|
||||
const s = item.skillCreated;
|
||||
return (
|
||||
<View style={[styles.messageBubble, styles.ariaBubble, {borderLeftWidth: 3, borderLeftColor: '#FFD60A'}]}>
|
||||
<View style={[styles.messageBubble, styles.ariaBubble, {borderLeftWidth: 3, borderLeftColor: '#FFD60A'}, searchHighlightStyle]}>
|
||||
<Text style={{color: '#FFD60A', fontWeight: 'bold', fontSize: 14}}>
|
||||
{'🛠 ARIA hat einen neuen Skill erstellt'}
|
||||
</Text>
|
||||
@@ -1168,7 +1210,7 @@ const ChatScreen: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.messageBubble, isUser ? styles.userBubble : styles.ariaBubble]}>
|
||||
<View style={[styles.messageBubble, isUser ? styles.userBubble : styles.ariaBubble, searchHighlightStyle]}>
|
||||
{/* Anhang-Vorschau */}
|
||||
{item.attachments?.map((att, idx) => (
|
||||
<View key={idx}>
|
||||
@@ -1342,7 +1384,7 @@ const ChatScreen: React.FC = () => {
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Suchleiste */}
|
||||
{/* Suchleiste mit Treffer-Navigation */}
|
||||
{searchVisible && (
|
||||
<View style={styles.searchBar}>
|
||||
<TextInput
|
||||
@@ -1353,17 +1395,43 @@ const ChatScreen: React.FC = () => {
|
||||
placeholderTextColor="#555570"
|
||||
autoFocus
|
||||
/>
|
||||
{searchQuery ? (
|
||||
<Text style={{color: searchMatchIds.length ? '#0096FF' : '#555570', fontSize: 12, paddingHorizontal: 6}}>
|
||||
{searchMatchIds.length ? `${searchIndex + 1}/${searchMatchIds.length}` : '0/0'}
|
||||
</Text>
|
||||
) : null}
|
||||
<TouchableOpacity
|
||||
onPress={gotoSearchPrev}
|
||||
disabled={!searchMatchIds.length}
|
||||
style={{paddingHorizontal: 6, opacity: searchMatchIds.length ? 1 : 0.3}}
|
||||
>
|
||||
<Text style={{color: '#0096FF', fontSize: 18}}>{'▲'}</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={gotoSearchNext}
|
||||
disabled={!searchMatchIds.length}
|
||||
style={{paddingHorizontal: 6, opacity: searchMatchIds.length ? 1 : 0.3}}
|
||||
>
|
||||
<Text style={{color: '#0096FF', fontSize: 18}}>{'▼'}</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => { setSearchVisible(false); setSearchQuery(''); }}>
|
||||
<Text style={{color: '#FF3B30', fontSize: 14, paddingHorizontal: 8}}>X</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Nachrichtenliste */}
|
||||
{/* Nachrichtenliste — Suche FILTERT NICHT mehr, sondern hebt aktiven
|
||||
Treffer hervor (siehe renderMessage: activeSearchId-Border). */}
|
||||
<FlatList
|
||||
ref={flatListRef}
|
||||
inverted
|
||||
data={searchQuery ? messages.filter(m => m.text.toLowerCase().includes(searchQuery.toLowerCase())).reverse() : invertedMessages}
|
||||
data={invertedMessages}
|
||||
onScrollToIndexFailed={(info) => {
|
||||
// Bei zu schnellem Aufruf vor Layout: einmal nachfassen
|
||||
setTimeout(() => {
|
||||
try { flatListRef.current?.scrollToIndex({ index: info.index, animated: true, viewPosition: 0.4 }); } catch {}
|
||||
}, 200);
|
||||
}}
|
||||
keyExtractor={item => item.id}
|
||||
renderItem={renderMessage}
|
||||
contentContainerStyle={styles.messageList}
|
||||
|
||||
Reference in New Issue
Block a user