|
1 | 1 | import { Stack } from "expo-router"; |
2 | | -import { useState } from "react"; |
3 | | -import { |
4 | | - KeyboardAvoidingView, |
5 | | - Platform, |
6 | | - Text, |
7 | | - TextInput, |
8 | | - TouchableOpacity, |
9 | | - View, |
10 | | -} 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"; |
11 | 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 { thread, streamingActive, askMax, stopGeneration } = useMaxStore(); |
| 12 | + const insets = useSafeAreaInsets(); |
| 13 | + const { thread, streamingActive, askMax, stopGeneration, resetThread } = |
| 14 | + useMaxStore(); |
17 | 15 |
|
18 | | - const handleSend = async () => { |
19 | | - const trimmed = inputText.trim(); |
20 | | - if (!trimmed) return; |
21 | | - setInputText(""); |
22 | | - await askMax(trimmed); |
23 | | - }; |
| 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 | + }, []); |
24 | 48 |
|
25 | 49 | return ( |
26 | 50 | <> |
27 | 51 | <Stack.Screen |
28 | 52 | options={{ |
29 | 53 | headerShown: true, |
30 | 54 | headerTitle: "Chat", |
| 55 | + headerBackTitle: "Back", |
31 | 56 | headerStyle: { backgroundColor: "#09090b" }, |
32 | 57 | headerTintColor: "#fff", |
33 | | - headerRight: streamingActive |
34 | | - ? () => ( |
35 | | - <TouchableOpacity onPress={stopGeneration}> |
36 | | - <Text className="text-red-500">Stop</Text> |
37 | | - </TouchableOpacity> |
38 | | - ) |
39 | | - : undefined, |
| 58 | + headerTitleStyle: { |
| 59 | + fontWeight: "600", |
| 60 | + }, |
| 61 | + headerRight, |
40 | 62 | }} |
41 | 63 | /> |
42 | | - <KeyboardAvoidingView |
43 | | - behavior={Platform.OS === "ios" ? "padding" : "height"} |
44 | | - className="flex-1 bg-black" |
45 | | - keyboardVerticalOffset={100} |
46 | | - > |
47 | | - <MessagesList messages={thread} isLoading={streamingActive} /> |
| 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 | + /> |
48 | 74 |
|
49 | | - {/* Input */} |
50 | | - <View className="flex-row items-center gap-2 border-gray-800 border-t p-4"> |
51 | | - <TextInput |
52 | | - className="flex-1 rounded-lg bg-gray-800 px-4 py-3 text-white" |
53 | | - placeholder="Type a message..." |
54 | | - placeholderTextColor="#666" |
55 | | - value={inputText} |
56 | | - onChangeText={setInputText} |
57 | | - onSubmitEditing={handleSend} |
58 | | - editable={!streamingActive} |
59 | | - /> |
60 | | - <TouchableOpacity |
61 | | - onPress={handleSend} |
62 | | - disabled={!inputText.trim() || streamingActive} |
63 | | - className={`rounded-lg px-4 py-3 ${ |
64 | | - inputText.trim() && !streamingActive |
65 | | - ? "bg-blue-600" |
66 | | - : "bg-gray-700" |
67 | | - }`} |
68 | | - > |
69 | | - <Text className="text-white">Send</Text> |
70 | | - </TouchableOpacity> |
| 75 | + {/* Fixed input at bottom */} |
| 76 | + <View className="absolute inset-x-0 bottom-0"> |
| 77 | + <ChatInput onSend={handleSend} disabled={streamingActive} /> |
71 | 78 | </View> |
72 | | - </KeyboardAvoidingView> |
| 79 | + </Animated.View> |
73 | 80 | </> |
74 | 81 | ); |
75 | 82 | } |
0 commit comments