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 [fullscreenImage, setFullscreenImage] = useState<string | null>(null);
|
||||||
const [memoryDetailId, setMemoryDetailId] = useState<string | null>(null);
|
const [memoryDetailId, setMemoryDetailId] = useState<string | null>(null);
|
||||||
const [inboxVisible, setInboxVisible] = useState(false);
|
const [inboxVisible, setInboxVisible] = useState(false);
|
||||||
|
const [showJumpDown, setShowJumpDown] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [searchVisible, setSearchVisible] = useState(false);
|
const [searchVisible, setSearchVisible] = useState(false);
|
||||||
const [searchIndex, setSearchIndex] = useState(0); // welcher Treffer aktiv ist
|
const [searchIndex, setSearchIndex] = useState(0); // welcher Treffer aktiv ist
|
||||||
@@ -1052,9 +1053,10 @@ const ChatScreen: React.FC = () => {
|
|||||||
}, [searchQuery]);
|
}, [searchQuery]);
|
||||||
|
|
||||||
// Bei Index-Wechsel zu der entsprechenden Bubble scrollen.
|
// Bei Index-Wechsel zu der entsprechenden Bubble scrollen.
|
||||||
// FlatList ist `inverted` → viewPosition 0.5 (mitte) ist beim inverted-Render
|
// FlatList ist `inverted`. viewPosition 0 = Item-Top oben am Viewport →
|
||||||
// tatsaechlich die Mitte des sichtbaren Bereichs. Wir verzoegern minimal
|
// Treffer-Bubble liegt mit dem Anfang direkt oben sichtbar, kein
|
||||||
// damit Layout sicher fertig ist.
|
// weiteres Hochscrollen noetig. Plus mehrere Retries da Layout bei
|
||||||
|
// langen Listen zeitversetzt fertig wird.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!searchMatchIds.length) return;
|
if (!searchMatchIds.length) return;
|
||||||
const id = searchMatchIds[searchIndex];
|
const id = searchMatchIds[searchIndex];
|
||||||
@@ -1063,13 +1065,16 @@ const ChatScreen: React.FC = () => {
|
|||||||
if (idx < 0 || !flatListRef.current) return;
|
if (idx < 0 || !flatListRef.current) return;
|
||||||
const tryScroll = () => {
|
const tryScroll = () => {
|
||||||
try {
|
try {
|
||||||
flatListRef.current?.scrollToIndex({ index: idx, animated: true, viewPosition: 0.5 });
|
flatListRef.current?.scrollToIndex({ index: idx, animated: true, viewPosition: 0 });
|
||||||
} catch {
|
} catch {
|
||||||
// wird von onScrollToIndexFailed nochmal versucht
|
// 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);
|
requestAnimationFrame(tryScroll);
|
||||||
|
[180, 420, 800].forEach(d => setTimeout(tryScroll, d));
|
||||||
}, [searchIndex, searchMatchIds, invertedMessages]);
|
}, [searchIndex, searchMatchIds, invertedMessages]);
|
||||||
|
|
||||||
const activeSearchId = searchMatchIds[searchIndex] || '';
|
const activeSearchId = searchMatchIds[searchIndex] || '';
|
||||||
@@ -1726,15 +1731,27 @@ const ChatScreen: React.FC = () => {
|
|||||||
ref={flatListRef}
|
ref={flatListRef}
|
||||||
inverted
|
inverted
|
||||||
data={invertedMessages}
|
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) => {
|
onScrollToIndexFailed={(info) => {
|
||||||
// FlatList kennt das Item-Layout noch nicht. Zuerst grob in die
|
// FlatList kennt das Item-Layout noch nicht. Zuerst grob in die
|
||||||
// Naehe scrollen (Average-Item-Hoehe-Schaetzung), dann nach 250ms
|
// Naehe scrollen (Average-Item-Hoehe-Schaetzung), dann mehrfach
|
||||||
// praezise nochmal versuchen.
|
// praezise nachsetzen — bei langem Chat braucht's manchmal mehrere
|
||||||
|
// Runden bis die Layouts gemessen sind.
|
||||||
const offset = info.averageItemLength * info.index;
|
const offset = info.averageItemLength * info.index;
|
||||||
try { flatListRef.current?.scrollToOffset({ offset, animated: false }); } catch {}
|
try { flatListRef.current?.scrollToOffset({ offset, animated: false }); } catch {}
|
||||||
setTimeout(() => {
|
// viewPosition 0 = Item-Top oben am Viewport → Stefan landet am
|
||||||
try { flatListRef.current?.scrollToIndex({ index: info.index, animated: true, viewPosition: 0.5 }); } catch {}
|
// Text-Anfang der Bubble, nicht in der Mitte oder am Ende.
|
||||||
}, 250);
|
[120, 320, 600].forEach(delay => {
|
||||||
|
setTimeout(() => {
|
||||||
|
try { flatListRef.current?.scrollToIndex({ index: info.index, animated: true, viewPosition: 0 }); } catch {}
|
||||||
|
}, delay);
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
keyExtractor={item => item.id}
|
keyExtractor={item => item.id}
|
||||||
renderItem={renderMessage}
|
renderItem={renderMessage}
|
||||||
@@ -1801,6 +1818,24 @@ const ChatScreen: React.FC = () => {
|
|||||||
</View>
|
</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 */}
|
{/* Eingabebereich */}
|
||||||
<View style={styles.inputContainer}>
|
<View style={styles.inputContainer}>
|
||||||
{/* Datei-Buttons */}
|
{/* Datei-Buttons */}
|
||||||
@@ -2341,6 +2376,23 @@ const styles = StyleSheet.create({
|
|||||||
color: '#555570',
|
color: '#555570',
|
||||||
fontSize: 10,
|
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: {
|
bubbleTrash: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 4,
|
top: 4,
|
||||||
|
|||||||
@@ -348,7 +348,6 @@ Skills mit Tool-Use.
|
|||||||
- [ ] Custom-Wake-Word-Upload via Diagnostic (eigene .onnx-Files ohne App-Rebuild)
|
- [ ] Custom-Wake-Word-Upload via Diagnostic (eigene .onnx-Files ohne App-Rebuild)
|
||||||
|
|
||||||
### Architektur
|
### Architektur
|
||||||
- [ ] Bilder: Claude Vision direkt nutzen (aktuell nur Dateipfad an ARIA)
|
|
||||||
- [ ] Diagnostic: System-Info Tab (Container-Status, Disk, RAM, CPU)
|
- [ ] Diagnostic: System-Info Tab (Container-Status, Disk, RAM, CPU)
|
||||||
- [ ] RVS Zombie-Connections endgueltig loesen
|
- [ ] RVS Zombie-Connections endgueltig loesen
|
||||||
- [ ] Gamebox: kleine Web-Oberflaeche fuer Credentials/Server-Config oder zentral aus Diagnostic per RVS push
|
- [ ] Gamebox: kleine Web-Oberflaeche fuer Credentials/Server-Config oder zentral aus Diagnostic per RVS push
|
||||||
|
|||||||
Reference in New Issue
Block a user