multica/apps/mobile/app/index.tsx
Naiyuan Qing 6037be2efa refactor: migrate from Hugeicons to Lucide icons
- Replace @hugeicons/react with lucide-react across all packages
- Update all components to use Lucide icon components
- Add silent option to store refresh methods to control toast display
- Simplify icon usage with direct component imports

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-12 10:12:56 +08:00

164 lines
5.5 KiB
TypeScript

import { useState, useRef, useCallback } from "react";
import {
View,
ScrollView,
KeyboardAvoidingView,
Platform,
TextInput,
Pressable,
LayoutAnimation,
UIManager,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { Text } from "@/components/ui/text";
import { ArrowUp } from "lucide-react-native";
// Enable LayoutAnimation on Android
if (Platform.OS === "android" && UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
const INPUT_MAX_HEIGHT = 120;
const SINGLE_LINE_HEIGHT = 20; // approximate single line content height
const MOCK_MESSAGES = [
{ id: "1", role: "user", content: "Hello, can you help me with a task?" },
{
id: "2",
role: "assistant",
content:
"Of course! I'd be happy to help. What would you like me to do?",
},
{ id: "3", role: "user", content: "Explain how WebSockets work." },
{
id: "4",
role: "assistant",
content:
"WebSockets provide full-duplex communication channels over a single TCP connection. Unlike HTTP, which is request-response based, WebSockets allow both the client and server to send messages independently at any time.\n\nThe connection starts as a standard HTTP request, then upgrades to a persistent WebSocket connection via a handshake.",
},
];
export default function Index() {
const [input, setInput] = useState("");
const [messages, setMessages] = useState(MOCK_MESSAGES);
const [contentHeight, setContentHeight] = useState(SINGLE_LINE_HEIGHT);
const scrollRef = useRef<ScrollView>(null);
const isMultiline = contentHeight > SINGLE_LINE_HEIGHT + 4;
const handleContentSizeChange = useCallback(
(e: { nativeEvent: { contentSize: { height: number } } }) => {
const newHeight = e.nativeEvent.contentSize.height;
if (newHeight !== contentHeight) {
LayoutAnimation.configureNext(
LayoutAnimation.create(150, "easeInEaseOut", "opacity")
);
setContentHeight(newHeight);
}
},
[contentHeight]
);
function handleSend() {
const trimmed = input.trim();
if (!trimmed) return;
setMessages((prev) => [
...prev,
{ id: String(Date.now()), role: "user", content: trimmed },
]);
setInput("");
LayoutAnimation.configureNext(
LayoutAnimation.create(150, "easeInEaseOut", "opacity")
);
setContentHeight(SINGLE_LINE_HEIGHT);
setTimeout(() => scrollRef.current?.scrollToEnd({ animated: true }), 100);
}
const canSend = input.trim().length > 0;
return (
<SafeAreaView className="flex-1 bg-background">
<KeyboardAvoidingView
className="flex-1"
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
{/* Header */}
<View className="px-4 py-3">
<Text className="text-lg font-semibold text-foreground">Multica</Text>
<Text className="text-xs text-muted-foreground">Agent connected</Text>
</View>
{/* Messages */}
<ScrollView
ref={scrollRef}
className="flex-1"
contentContainerClassName="px-4 py-2 gap-5"
keyboardShouldPersistTaps="handled"
>
{messages.map((msg) =>
msg.role === "user" ? (
<View key={msg.id} className="flex-row justify-end">
<View className="max-w-[80%] rounded-2xl rounded-br-sm bg-muted px-4 py-2.5">
<Text className="text-[15px] leading-[22px] text-foreground">
{msg.content}
</Text>
</View>
</View>
) : (
<View key={msg.id}>
<Text className="text-[15px] leading-[22px] text-foreground">
{msg.content}
</Text>
</View>
)
)}
</ScrollView>
{/* Input bar */}
<View className="px-3 pb-1 pt-2">
<View
className={`flex-row border border-border bg-card pl-4 pr-1.5 py-1.5 ${
isMultiline ? "items-end rounded-2xl" : "items-center rounded-full"
}`}
>
<TextInput
className="flex-1 border-0 bg-transparent text-[15px] leading-5 text-foreground"
placeholder="Type a message..."
placeholderTextColor="hsl(240.1, 4.4%, 46.3%)"
value={input}
onChangeText={setInput}
onSubmitEditing={handleSend}
onContentSizeChange={handleContentSizeChange}
multiline
scrollEnabled={contentHeight >= INPUT_MAX_HEIGHT}
textAlignVertical="top"
selectionColor="hsl(243.5, 75.2%, 58.5%)"
underlineColorAndroid="transparent"
style={{
maxHeight: INPUT_MAX_HEIGHT,
paddingTop: 0,
paddingBottom: 0,
// @ts-ignore web-only: remove browser default outline/border
outline: "none",
borderWidth: 0,
boxShadow: "none",
}}
/>
<Pressable
onPress={handleSend}
disabled={!canSend}
className="ml-1.5 h-8 w-8 items-center justify-center rounded-full bg-primary"
style={{ opacity: canSend ? 1 : 0.5 }}
>
<ArrowUp
size={18}
color="hsl(225, 100%, 96.4%)"
/>
</Pressable>
</View>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
);
}