|
1 | 1 | import { Stack } from "expo-router"; |
2 | | -import { useState } from "react"; |
3 | | -import { |
4 | | - KeyboardAvoidingView, |
5 | | - Platform, |
6 | | - ScrollView, |
7 | | - Text, |
8 | | - TextInput, |
9 | | - TouchableOpacity, |
10 | | - View, |
11 | | -} from "react-native"; |
| 2 | +import { useCallback } from "react"; |
| 3 | +import { Text, TouchableOpacity, View } from "react-native"; |
| 4 | +import Animated, { useAnimatedStyle } from "react-native-reanimated"; |
| 5 | +import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 6 | +import { ChatInput } from "../components/ChatInput"; |
| 7 | +import { MessagesList } from "../components/MessagesList"; |
| 8 | +import { useGradualAnimation } from "../hooks/useGradualAnimation"; |
12 | 9 | import { useMaxStore } from "../stores/maxStore"; |
13 | 10 |
|
14 | 11 | export default function ChatScreen() { |
15 | | - const [inputText, setInputText] = useState(""); |
16 | | - const { |
17 | | - thread, |
18 | | - conversation, |
19 | | - streamingActive, |
20 | | - askMax, |
21 | | - stopGeneration, |
22 | | - resetThread, |
23 | | - } = useMaxStore(); |
| 12 | + const insets = useSafeAreaInsets(); |
| 13 | + const { thread, streamingActive, askMax, stopGeneration, resetThread } = |
| 14 | + useMaxStore(); |
24 | 15 |
|
25 | | - const handleSend = async () => { |
26 | | - const trimmed = inputText.trim(); |
27 | | - if (!trimmed) return; |
28 | | - setInputText(""); |
29 | | - await askMax(trimmed); |
30 | | - }; |
| 16 | + const handleSend = useCallback( |
| 17 | + async (message: string) => { |
| 18 | + await askMax(message); |
| 19 | + }, |
| 20 | + [askMax], |
| 21 | + ); |
| 22 | + |
| 23 | + const headerRight = useCallback(() => { |
| 24 | + if (streamingActive) { |
| 25 | + return ( |
| 26 | + <TouchableOpacity onPress={stopGeneration} className="px-2"> |
| 27 | + <Text className="font-medium text-red-500">Stop</Text> |
| 28 | + </TouchableOpacity> |
| 29 | + ); |
| 30 | + } |
| 31 | + if (thread.length > 0) { |
| 32 | + return ( |
| 33 | + <TouchableOpacity onPress={resetThread} className="px-2"> |
| 34 | + <Text className="font-medium text-orange-500">New</Text> |
| 35 | + </TouchableOpacity> |
| 36 | + ); |
| 37 | + } |
| 38 | + return null; |
| 39 | + }, [streamingActive, thread.length, stopGeneration, resetThread]); |
| 40 | + |
| 41 | + const { height } = useGradualAnimation(); |
| 42 | + |
| 43 | + const contentPosition = useAnimatedStyle(() => { |
| 44 | + return { |
| 45 | + transform: [{ translateY: -height.value }], |
| 46 | + }; |
| 47 | + }, []); |
31 | 48 |
|
32 | 49 | return ( |
33 | 50 | <> |
34 | 51 | <Stack.Screen |
35 | 52 | options={{ |
36 | 53 | headerShown: true, |
37 | 54 | headerTitle: "Chat", |
| 55 | + headerBackTitle: "Back", |
38 | 56 | headerStyle: { backgroundColor: "#09090b" }, |
39 | 57 | headerTintColor: "#fff", |
40 | | - headerRight: streamingActive |
41 | | - ? () => ( |
42 | | - <TouchableOpacity onPress={stopGeneration}> |
43 | | - <Text className="text-red-500">Stop</Text> |
44 | | - </TouchableOpacity> |
45 | | - ) |
46 | | - : undefined, |
| 58 | + headerTitleStyle: { |
| 59 | + fontWeight: "600", |
| 60 | + }, |
| 61 | + headerRight, |
47 | 62 | }} |
48 | 63 | /> |
49 | | - <KeyboardAvoidingView |
50 | | - behavior={Platform.OS === "ios" ? "padding" : "height"} |
51 | | - className="flex-1 bg-black" |
52 | | - keyboardVerticalOffset={100} |
53 | | - > |
54 | | - {/* JSON Output */} |
55 | | - <ScrollView className="flex-1 p-4"> |
56 | | - {/* Conversation metadata */} |
57 | | - {conversation && ( |
58 | | - <View className="mb-4"> |
59 | | - <Text className="mb-1 font-bold text-green-400"> |
60 | | - Conversation: |
61 | | - </Text> |
62 | | - <Text className="font-mono text-green-300 text-xs"> |
63 | | - {JSON.stringify(conversation, null, 2)} |
64 | | - </Text> |
65 | | - </View> |
66 | | - )} |
67 | | - |
68 | | - {/* Status */} |
69 | | - <View className="mb-4"> |
70 | | - <Text className="text-gray-400"> |
71 | | - Streaming: {streamingActive ? "true" : "false"} |
72 | | - </Text> |
73 | | - <Text className="text-gray-400">Messages: {thread.length}</Text> |
74 | | - </View> |
75 | | - |
76 | | - {/* Messages */} |
77 | | - {thread.map((message, index) => ( |
78 | | - <View key={message.id || `msg-${index}`} className="mb-4"> |
79 | | - <Text className="mb-1 font-bold text-yellow-400"> |
80 | | - [{index}] {message.type} ({message.status}) |
81 | | - </Text> |
82 | | - <Text className="font-mono text-white text-xs"> |
83 | | - {JSON.stringify(message, null, 2)} |
84 | | - </Text> |
85 | | - </View> |
86 | | - ))} |
87 | | - |
88 | | - {thread.length === 0 && !streamingActive && ( |
89 | | - <Text className="text-center text-gray-500"> |
90 | | - Send a message to start |
91 | | - </Text> |
92 | | - )} |
93 | | - |
94 | | - {thread.length > 0 && !streamingActive && ( |
95 | | - <TouchableOpacity onPress={resetThread} className="mt-4 py-2"> |
96 | | - <Text className="text-center text-blue-500 underline"> |
97 | | - Start a new chat |
98 | | - </Text> |
99 | | - </TouchableOpacity> |
100 | | - )} |
101 | | - </ScrollView> |
| 64 | + <Animated.View className="flex-1 bg-dark-bg" style={[contentPosition]}> |
| 65 | + <MessagesList |
| 66 | + messages={thread} |
| 67 | + streamingActive={streamingActive} |
| 68 | + contentContainerStyle={{ |
| 69 | + paddingTop: 80 + insets.bottom, |
| 70 | + paddingBottom: 16, |
| 71 | + flexGrow: thread.length === 0 ? 1 : undefined, |
| 72 | + }} |
| 73 | + /> |
102 | 74 |
|
103 | | - {/* Input */} |
104 | | - <View className="flex-row items-center gap-2 border-gray-800 border-t p-4"> |
105 | | - <TextInput |
106 | | - className="flex-1 rounded-lg bg-gray-800 px-4 py-3 text-white" |
107 | | - placeholder="Type a message..." |
108 | | - placeholderTextColor="#666" |
109 | | - value={inputText} |
110 | | - onChangeText={setInputText} |
111 | | - onSubmitEditing={handleSend} |
112 | | - editable={!streamingActive} |
113 | | - /> |
114 | | - <TouchableOpacity |
115 | | - onPress={handleSend} |
116 | | - disabled={!inputText.trim() || streamingActive} |
117 | | - className={`rounded-lg px-4 py-3 ${ |
118 | | - inputText.trim() && !streamingActive |
119 | | - ? "bg-blue-600" |
120 | | - : "bg-gray-700" |
121 | | - }`} |
122 | | - > |
123 | | - <Text className="text-white">Send</Text> |
124 | | - </TouchableOpacity> |
| 75 | + {/* Fixed input at bottom */} |
| 76 | + <View className="absolute inset-x-0 bottom-0"> |
| 77 | + <ChatInput onSend={handleSend} disabled={streamingActive} /> |
125 | 78 | </View> |
126 | | - </KeyboardAvoidingView> |
| 79 | + </Animated.View> |
127 | 80 | </> |
128 | 81 | ); |
129 | 82 | } |
0 commit comments