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.
78 lines
3.2 KiB
TypeScript
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,
|
|
},
|
|
});
|