feat(chat): Jump-down-Button + Sprung-an-Text-Anfang + Vision-Issue raus
Drei kleine UX-Fixes im Chat: 1. Jump-Down-Button (↓): Bei inverted FlatList erscheint rechts ueber der Eingabe ein blauer FAB, sobald man mehr als 250px von der neuesten Nachricht weg gescrollt ist. Tap → scrollToOffset(0) animated → wieder unten. Auto-hide wenn man unten ist. 2. Such-Sprung landet jetzt am TEXT-ANFANG der Treffer-Bubble: viewPosition 0.5 (Mitte) → 0 (Item-Top am Viewport-Top). Plus Retry-Folge (180/420/800ms) gegen Layout-Race bei langen Listen. Vorher musste man oft nochmal hoch scrollen um den Anfang zu sehen. onScrollToIndexFailed-Fallback genauso mit viewPosition 0. 3. issue.md: "Bilder: Claude Vision direkt nutzen" raus aus den offenen Punkten — ist durch Stufe E (Memory-Anhaenge, Read-Tool multi-modal) längst geloest. ARIA sieht Bilder echt. Folge-Etappen: Such-Sprung-Resilienz war Teil davon (mehrere Retries abgedeckt). Naechste Brocken: Doppel-Send-Haenger, AsyncStorage-Race, Offline-Queue mit Idempotenz. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -236,6 +236,7 @@ const ChatScreen: React.FC = () => {
|
||||
const [fullscreenImage, setFullscreenImage] = useState<string | null>(null);
|
||||
const [memoryDetailId, setMemoryDetailId] = useState<string | null>(null);
|
||||
const [inboxVisible, setInboxVisible] = useState(false);
|
||||
const [showJumpDown, setShowJumpDown] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [searchVisible, setSearchVisible] = useState(false);
|
||||
const [searchIndex, setSearchIndex] = useState(0); // welcher Treffer aktiv ist
|
||||
@@ -1052,9 +1053,10 @@ const ChatScreen: React.FC = () => {
|
||||
}, [searchQuery]);
|
||||
|
||||
// Bei Index-Wechsel zu der entsprechenden Bubble scrollen.
|
||||
// FlatList ist `inverted` → viewPosition 0.5 (mitte) ist beim inverted-Render
|
||||
// tatsaechlich die Mitte des sichtbaren Bereichs. Wir verzoegern minimal
|
||||
// damit Layout sicher fertig ist.
|
||||
// FlatList ist `inverted`. viewPosition 0 = Item-Top oben am Viewport →
|
||||
// Treffer-Bubble liegt mit dem Anfang direkt oben sichtbar, kein
|
||||
// weiteres Hochscrollen noetig. Plus mehrere Retries da Layout bei
|
||||
// langen Listen zeitversetzt fertig wird.
|
||||
useEffect(() => {
|
||||
if (!searchMatchIds.length) return;
|
||||
const id = searchMatchIds[searchIndex];
|
||||
@@ -1063,13 +1065,16 @@ const ChatScreen: React.FC = () => {
|
||||
if (idx < 0 || !flatListRef.current) return;
|
||||
const tryScroll = () => {
|
||||
try {
|
||||
flatListRef.current?.scrollToIndex({ index: idx, animated: true, viewPosition: 0.5 });
|
||||
flatListRef.current?.scrollToIndex({ index: idx, animated: true, viewPosition: 0 });
|
||||
} catch {
|
||||
// wird von onScrollToIndexFailed nochmal versucht
|
||||
}
|
||||
};
|
||||
// requestAnimationFrame statt setTimeout 0 — wartet auf naechsten Layout-Frame
|
||||
// requestAnimationFrame fuer den ersten Versuch, dann setTimeout-Folge
|
||||
// damit auch bei tiefen Indizes (viel ungelayoutete Items dazwischen)
|
||||
// der Sprung am Ende sitzt.
|
||||
requestAnimationFrame(tryScroll);
|
||||
[180, 420, 800].forEach(d => setTimeout(tryScroll, d));
|
||||
}, [searchIndex, searchMatchIds, invertedMessages]);
|
||||
|
||||
const activeSearchId = searchMatchIds[searchIndex] || '';
|
||||
@@ -1726,15 +1731,27 @@ const ChatScreen: React.FC = () => {
|
||||
ref={flatListRef}
|
||||
inverted
|
||||
data={invertedMessages}
|
||||
onScroll={(e) => {
|
||||
// Bei inverted FlatList: contentOffset.y > 0 = weg von "unten"
|
||||
// (= aelter scrollen). Wir zeigen den Jump-Down-Button ab ~250px.
|
||||
const y = e.nativeEvent.contentOffset.y;
|
||||
setShowJumpDown(y > 250);
|
||||
}}
|
||||
scrollEventThrottle={120}
|
||||
onScrollToIndexFailed={(info) => {
|
||||
// FlatList kennt das Item-Layout noch nicht. Zuerst grob in die
|
||||
// Naehe scrollen (Average-Item-Hoehe-Schaetzung), dann nach 250ms
|
||||
// praezise nochmal versuchen.
|
||||
// Naehe scrollen (Average-Item-Hoehe-Schaetzung), dann mehrfach
|
||||
// praezise nachsetzen — bei langem Chat braucht's manchmal mehrere
|
||||
// Runden bis die Layouts gemessen sind.
|
||||
const offset = info.averageItemLength * info.index;
|
||||
try { flatListRef.current?.scrollToOffset({ offset, animated: false }); } catch {}
|
||||
setTimeout(() => {
|
||||
try { flatListRef.current?.scrollToIndex({ index: info.index, animated: true, viewPosition: 0.5 }); } catch {}
|
||||
}, 250);
|
||||
// viewPosition 0 = Item-Top oben am Viewport → Stefan landet am
|
||||
// Text-Anfang der Bubble, nicht in der Mitte oder am Ende.
|
||||
[120, 320, 600].forEach(delay => {
|
||||
setTimeout(() => {
|
||||
try { flatListRef.current?.scrollToIndex({ index: info.index, animated: true, viewPosition: 0 }); } catch {}
|
||||
}, delay);
|
||||
});
|
||||
}}
|
||||
keyExtractor={item => item.id}
|
||||
renderItem={renderMessage}
|
||||
@@ -1801,6 +1818,24 @@ const ChatScreen: React.FC = () => {
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Jump-to-Bottom-Button — erscheint wenn man weg von der neuesten
|
||||
Nachricht gescrollt hat. Bei inverted FlatList ist scrollToOffset
|
||||
0 == neueste Nachricht visuell unten. */}
|
||||
{showJumpDown && (
|
||||
<TouchableOpacity
|
||||
style={styles.jumpDownBtn}
|
||||
activeOpacity={0.85}
|
||||
onPress={() => {
|
||||
try {
|
||||
flatListRef.current?.scrollToOffset({ offset: 0, animated: true });
|
||||
} catch {}
|
||||
setShowJumpDown(false);
|
||||
}}
|
||||
>
|
||||
<Text style={{color:'#fff', fontSize:18, fontWeight:'700'}}>{'↓'}</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{/* Eingabebereich */}
|
||||
<View style={styles.inputContainer}>
|
||||
{/* Datei-Buttons */}
|
||||
@@ -2341,6 +2376,23 @@ const styles = StyleSheet.create({
|
||||
color: '#555570',
|
||||
fontSize: 10,
|
||||
},
|
||||
jumpDownBtn: {
|
||||
position: 'absolute',
|
||||
right: 16,
|
||||
bottom: 80,
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
backgroundColor: '#0096FF',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.4,
|
||||
shadowRadius: 4,
|
||||
elevation: 5,
|
||||
zIndex: 100,
|
||||
},
|
||||
bubbleTrash: {
|
||||
position: 'absolute',
|
||||
top: 4,
|
||||
|
||||
@@ -348,7 +348,6 @@ Skills mit Tool-Use.
|
||||
- [ ] Custom-Wake-Word-Upload via Diagnostic (eigene .onnx-Files ohne App-Rebuild)
|
||||
|
||||
### Architektur
|
||||
- [ ] Bilder: Claude Vision direkt nutzen (aktuell nur Dateipfad an ARIA)
|
||||
- [ ] Diagnostic: System-Info Tab (Container-Status, Disk, RAM, CPU)
|
||||
- [ ] RVS Zombie-Connections endgueltig loesen
|
||||
- [ ] Gamebox: kleine Web-Oberflaeche fuer Credentials/Server-Config oder zentral aus Diagnostic per RVS push
|
||||
|
||||
Reference in New Issue
Block a user