Files
LocalMind/app/chat/index.tsx
evandro cb6eda0562 refactor(chat): split ChatScreen and reuse regenerateResponse after editing a message
The edit path duplicated the streaming flow with inline require() calls; it now reuses regenerateResponse, which also fixes:
- edited conversations were never saved to History
- the answer generated after an edit could not be stopped (no abort handle kept)
Stopping a stream now saves the partial conversation, like finishing or failing does.
Adds characterization tests for ChatScreen and useLMStudio.
2026-10-10 04:16:29 -03:00

78 lines
3.2 KiB
TypeScript

import { ChatInput } from '@/components/chat/ChatInput';
import { EmptyState } from '@/components/chat/EmptyState';
import { MessageList } from '@/components/chat/MessageList';
import { OfflineBanner } from '@/components/chat/OfflineBanner';
import { useLMStudio } from '@/hooks/useLMStudio';
import { useThemeColor } from '@/hooks/useThemeColor';
import { useAppStore } from '@/store/useAppStore';
import React, { useCallback } from 'react';
import { KeyboardAvoidingView, Platform, StyleSheet } from 'react-native';
export default function ChatScreen() {
const colors = useThemeColor(); // Dynamic theme
const messages = useAppStore(state => state.messages);
const editingMessageId = useAppStore(state => state.editingMessageId);
const setEditingMessageId = useAppStore(state => state.setEditingMessageId);
const deleteMessagesAfter = useAppStore(state => state.deleteMessagesAfter);
const deleteMessage = useAppStore(state => state.deleteMessage);
const editMessage = useAppStore(state => state.editMessage);
const { sendMessage, stopStreaming, regenerateResponse, isStreaming, isConnected } = useLMStudio();
// Find the editing message content
const editingMessage = editingMessageId
? messages.find(m => m.id === editingMessageId)
: null;
const handleSend = useCallback((text: string) => {
if (editingMessageId && editingMessage) {
editMessage(editingMessageId, text);
deleteMessagesAfter(editingMessageId);
setEditingMessageId(null);
// The edited message is now the last one, so regenerating answers it.
regenerateResponse();
} else {
sendMessage(text);
}
}, [editingMessageId, editingMessage, editMessage, deleteMessagesAfter, setEditingMessageId, regenerateResponse, sendMessage]);
const handleEdit = useCallback((id: string) => setEditingMessageId(id), [setEditingMessageId]);
const handleCancelEdit = useCallback(() => setEditingMessageId(null), [setEditingMessageId]);
const handleDelete = useCallback((id: string) => deleteMessage(id), [deleteMessage]);
return (
<KeyboardAvoidingView
style={[styles.container, { backgroundColor: colors.backgroundColor }]}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={Platform.OS === 'ios' ? 90 : 0}
>
{!isConnected && <OfflineBanner />}
{messages.length === 0 ? (
<EmptyState onSelectPrompt={sendMessage} />
) : (
<MessageList
messages={messages}
isStreaming={isStreaming}
onEdit={handleEdit}
onDelete={handleDelete}
onRegenerate={regenerateResponse}
/>
)}
<ChatInput
onSend={handleSend}
onStop={stopStreaming}
isLoading={isStreaming}
editingText={editingMessage?.content ?? null}
onCancelEdit={handleCancelEdit}
/>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
});