Skip to content

Commit 2f243d5

Browse files
committed
Merge branch 'mobile-app' of github.com:PostHog/Array into mobile-app
2 parents fcd82ed + d9b3787 commit 2f243d5

File tree

22 files changed

+1069
-87
lines changed

22 files changed

+1069
-87
lines changed

apps/mobile/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
"dependencies": {
1616
"@react-native-async-storage/async-storage": "^2.2.0",
1717
"@tanstack/react-query": "^5.90.12",
18+
"date-fns": "^4.1.0",
1819
"expo": "~54.0.27",
19-
"react-native-webview": "^13.13.5",
2020
"expo-auth-session": "^7.0.10",
2121
"expo-constants": "~18.0.11",
2222
"expo-crypto": "^15.0.8",
@@ -36,6 +36,7 @@
3636
"react-native-reanimated": "~4.1.1",
3737
"react-native-safe-area-context": "~5.6.2",
3838
"react-native-screens": "~4.16.0",
39+
"react-native-webview": "^13.13.5",
3940
"zustand": "^4.5.7"
4041
},
4142
"devDependencies": {

apps/mobile/src/app/(tabs)/_layout.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,20 @@ export default function TabsLayout() {
2727
tintColor={dynamicTintColor}
2828
minimizeBehavior="onScrollDown"
2929
>
30-
{/* Tasks - Home Tab */}
30+
{/* Conversations - First Tab (default landing) */}
3131
<NativeTabs.Trigger name="index">
32+
<Label>Chats</Label>
33+
<Icon
34+
sf={{
35+
default: "bubble.left.and.bubble.right",
36+
selected: "bubble.left.and.bubble.right.fill",
37+
}}
38+
drawable="ic_menu_send"
39+
/>
40+
</NativeTabs.Trigger>
41+
42+
{/* Tasks Tab */}
43+
<NativeTabs.Trigger name="tasks">
3244
<Label>Tasks</Label>
3345
<Icon
3446
sf={{ default: "checklist", selected: "checklist" }}
Lines changed: 29 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,44 @@
11
import { useRouter } from "expo-router";
2-
import { Pressable, ScrollView, Text, View } from "react-native";
3-
import { useAuthStore } from "../../stores/authStore";
4-
5-
export default function TasksScreen() {
2+
import { Pressable, View } from "react-native";
3+
import { Text } from "../../components/text";
4+
import {
5+
type ConversationDetail,
6+
ConversationList,
7+
} from "../../features/conversations";
8+
9+
export default function ConversationsScreen() {
610
const router = useRouter();
7-
const { cloudRegion, projectId } = useAuthStore();
811

9-
const handleCreateTask = () => {
10-
router.push("/agent");
12+
const handleConversationPress = (conversation: ConversationDetail) => {
13+
router.push(`/conversation/${conversation.id}`);
1114
};
1215

13-
const handleChatWithAI = () => {
16+
const handleNewChat = () => {
1417
router.push("/chat");
1518
};
1619

1720
return (
18-
<ScrollView className="flex-1 bg-dark-bg">
19-
<View className="px-6 pt-16 pb-32">
20-
{/* Header */}
21-
<View className="mb-10">
22-
<Text className="mb-2 font-bold text-3xl text-white">Tasks</Text>
23-
<Text className="text-base text-dark-text-muted">
24-
Your PostHog tasks
25-
</Text>
26-
</View>
27-
28-
{/* Create New Task Button */}
29-
<Pressable
30-
onPress={handleCreateTask}
31-
className="mb-4 items-center rounded-xl bg-orange-500 py-4 active:bg-orange-600"
32-
>
33-
<Text className="font-semibold text-base text-white">
34-
Create new task
35-
</Text>
36-
</Pressable>
37-
38-
{/* Chat with PostHog AI Button */}
39-
<Pressable
40-
onPress={handleChatWithAI}
41-
className="mb-6 items-center rounded-xl bg-blue-600 py-4 active:bg-blue-700"
42-
>
43-
<Text className="font-semibold text-base text-white">
44-
Chat with PostHog AI
45-
</Text>
46-
</Pressable>
47-
48-
{/* Info Card */}
49-
<View className="mb-6 rounded-xl bg-dark-surface p-4">
50-
<View className="flex-row justify-between py-2">
51-
<Text className="text-dark-text-muted text-sm">Region</Text>
52-
<Text className="font-medium text-sm text-white">
53-
{cloudRegion?.toUpperCase() || "N/A"}
54-
</Text>
55-
</View>
56-
<View className="flex-row justify-between py-2">
57-
<Text className="text-dark-text-muted text-sm">Project ID</Text>
58-
<Text className="font-medium text-sm text-white">
59-
{projectId || "N/A"}
21+
<View className="flex-1 bg-dark-bg">
22+
{/* Header */}
23+
<View className="border-dark-border border-b px-4 pt-16 pb-4">
24+
<View className="flex-row items-center justify-between">
25+
<View>
26+
<Text className="font-bold text-2xl text-white">Conversations</Text>
27+
<Text className="text-dark-text-muted text-sm">
28+
Your Max AI chats
6029
</Text>
6130
</View>
62-
</View>
63-
64-
{/* Empty State */}
65-
<View className="flex-1 items-center justify-center py-20">
66-
<Text className="text-base text-dark-text-muted">No tasks yet</Text>
31+
<Pressable
32+
onPress={handleNewChat}
33+
className="rounded-lg bg-orange-500 px-4 py-2 active:bg-orange-600"
34+
>
35+
<Text className="font-semibold text-sm text-white">New chat</Text>
36+
</Pressable>
6737
</View>
6838
</View>
69-
</ScrollView>
39+
40+
{/* Conversation List */}
41+
<ConversationList onConversationPress={handleConversationPress} />
42+
</View>
7043
);
7144
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useRouter } from "expo-router";
2+
import { Pressable, View } from "react-native";
3+
import { Text } from "../../components/text";
4+
import { TaskList } from "../../features/tasks/components/TaskList";
5+
6+
export default function TasksScreen() {
7+
const router = useRouter();
8+
9+
const handleCreateTask = () => {
10+
router.push("/agent");
11+
};
12+
13+
return (
14+
<View className="flex-1 bg-dark-bg">
15+
{/* Header */}
16+
<View className="border-dark-border border-b px-4 pt-16 pb-4">
17+
<View className="flex-row items-center justify-between">
18+
<View>
19+
<Text className="font-bold text-2xl text-white">Tasks</Text>
20+
<Text className="text-dark-text-muted text-sm">
21+
Your PostHog tasks
22+
</Text>
23+
</View>
24+
<Pressable
25+
onPress={handleCreateTask}
26+
className="rounded-lg bg-orange-500 px-4 py-2 active:bg-orange-600"
27+
>
28+
<Text className="font-semibold text-sm text-white">New task</Text>
29+
</Pressable>
30+
</View>
31+
</View>
32+
33+
{/* Task List */}
34+
<TaskList />
35+
</View>
36+
);
37+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
2+
import { useCallback, useEffect, useState } from "react";
3+
import {
4+
ActivityIndicator,
5+
Pressable,
6+
Text,
7+
TouchableOpacity,
8+
View,
9+
} from "react-native";
10+
import Animated, { useAnimatedStyle } from "react-native-reanimated";
11+
import { useSafeAreaInsets } from "react-native-safe-area-context";
12+
import { ChatInput } from "../../components/ChatInput";
13+
import { MessagesList } from "../../components/MessagesList";
14+
import { useGradualAnimation } from "../../hooks/useGradualAnimation";
15+
import { useMaxStore } from "../../stores/maxStore";
16+
17+
export default function ConversationDetailScreen() {
18+
const { id } = useLocalSearchParams<{ id: string }>();
19+
const router = useRouter();
20+
const insets = useSafeAreaInsets();
21+
const [loadError, setLoadError] = useState<string | null>(null);
22+
23+
const {
24+
conversation,
25+
thread,
26+
streamingActive,
27+
conversationLoading,
28+
askMax,
29+
stopGeneration,
30+
loadConversation,
31+
resetThread,
32+
} = useMaxStore();
33+
34+
useEffect(() => {
35+
if (!id) return;
36+
37+
setLoadError(null);
38+
loadConversation(id).catch((err) => {
39+
console.error("Failed to load conversation:", err);
40+
setLoadError("Failed to load conversation");
41+
});
42+
43+
return () => {
44+
// Reset when leaving the screen
45+
resetThread();
46+
};
47+
}, [id, loadConversation, resetThread]);
48+
49+
const handleSend = useCallback(
50+
async (message: string) => {
51+
await askMax(message);
52+
},
53+
[askMax],
54+
);
55+
56+
const headerRight = useCallback(() => {
57+
if (streamingActive) {
58+
return (
59+
<TouchableOpacity onPress={stopGeneration} className="px-2">
60+
<Text className="font-medium text-red-500">Stop</Text>
61+
</TouchableOpacity>
62+
);
63+
}
64+
return null;
65+
}, [streamingActive, stopGeneration]);
66+
67+
const { height } = useGradualAnimation();
68+
69+
const contentPosition = useAnimatedStyle(() => {
70+
return {
71+
transform: [{ translateY: -height.value }],
72+
};
73+
}, []);
74+
75+
if (loadError) {
76+
return (
77+
<>
78+
<Stack.Screen
79+
options={{
80+
headerShown: true,
81+
headerTitle: "Error",
82+
headerBackTitle: "Back",
83+
headerStyle: { backgroundColor: "#09090b" },
84+
headerTintColor: "#fff",
85+
}}
86+
/>
87+
<View className="flex-1 items-center justify-center bg-dark-bg px-4">
88+
<Text className="mb-4 text-center text-red-400">{loadError}</Text>
89+
<Pressable
90+
onPress={() => router.back()}
91+
className="rounded-lg bg-dark-surface px-4 py-2"
92+
>
93+
<Text className="text-white">Go Back</Text>
94+
</Pressable>
95+
</View>
96+
</>
97+
);
98+
}
99+
100+
if (conversationLoading && thread.length === 0) {
101+
return (
102+
<>
103+
<Stack.Screen
104+
options={{
105+
headerShown: true,
106+
headerTitle: "Loading...",
107+
headerBackTitle: "Back",
108+
headerStyle: { backgroundColor: "#09090b" },
109+
headerTintColor: "#fff",
110+
}}
111+
/>
112+
<View className="flex-1 items-center justify-center bg-dark-bg">
113+
<ActivityIndicator size="large" color="#f97316" />
114+
<Text className="mt-4 text-dark-text-muted">
115+
Loading conversation...
116+
</Text>
117+
</View>
118+
</>
119+
);
120+
}
121+
122+
return (
123+
<>
124+
<Stack.Screen
125+
options={{
126+
headerShown: true,
127+
headerTitle: conversation?.title || "Conversation",
128+
headerBackTitle: "Back",
129+
headerStyle: { backgroundColor: "#09090b" },
130+
headerTintColor: "#fff",
131+
headerTitleStyle: {
132+
fontWeight: "600",
133+
},
134+
headerRight,
135+
}}
136+
/>
137+
<Animated.View className="flex-1 bg-dark-bg" style={[contentPosition]}>
138+
<MessagesList
139+
messages={thread}
140+
streamingActive={streamingActive}
141+
contentContainerStyle={{
142+
paddingTop: 80 + insets.bottom,
143+
paddingBottom: 16,
144+
flexGrow: thread.length === 0 ? 1 : undefined,
145+
}}
146+
/>
147+
148+
{/* Fixed input at bottom */}
149+
<View className="absolute inset-x-0 bottom-0">
150+
<ChatInput onSend={handleSend} disabled={streamingActive} />
151+
</View>
152+
</Animated.View>
153+
</>
154+
);
155+
}
Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,50 @@
11
import { ActivityIndicator, Text, View } from "react-native";
2+
import type { AssistantToolCall } from "../types/max";
3+
import { ToolCallMessage } from "./ToolCallMessage";
24

35
interface AIMessageProps {
46
content: string;
57
isLoading?: boolean;
68
thinkingText?: string;
9+
toolCalls?: AssistantToolCall[];
710
}
811

912
export function AIMessage({
1013
content,
1114
isLoading,
1215
thinkingText,
16+
toolCalls,
1317
}: AIMessageProps) {
1418
return (
15-
<View className="items-start px-4 py-2">
16-
<View className="max-w-[85%] rounded-2xl rounded-bl-md bg-dark-surface px-4 py-3">
17-
{isLoading && !content ? (
18-
<View className="flex-row items-center gap-2">
19-
<ActivityIndicator size="small" color="#a3a3a3" />
20-
<Text className="text-base text-dark-text-muted italic">
21-
{thinkingText || "Thinking..."}
19+
<View className="items-start py-2">
20+
{toolCalls && toolCalls.length > 0 && (
21+
<View className="mb-1 w-full">
22+
{toolCalls.map((tc) => (
23+
<ToolCallMessage
24+
key={tc.id}
25+
toolName={tc.name}
26+
status="completed"
27+
args={tc.args}
28+
/>
29+
))}
30+
</View>
31+
)}
32+
{(content || isLoading) && (
33+
<View className="mx-4 max-w-[85%] rounded-2xl rounded-bl-md bg-dark-surface px-4 py-3">
34+
{isLoading && !content ? (
35+
<View className="flex-row items-center gap-2">
36+
<ActivityIndicator size="small" color="#a3a3a3" />
37+
<Text className="text-base text-dark-text-muted italic">
38+
{thinkingText || "Thinking..."}
39+
</Text>
40+
</View>
41+
) : (
42+
<Text className="text-base text-dark-text leading-6">
43+
{content}
2244
</Text>
23-
</View>
24-
) : (
25-
<Text className="text-base text-dark-text leading-6">{content}</Text>
26-
)}
27-
</View>
45+
)}
46+
</View>
47+
)}
2848
</View>
2949
);
3050
}

0 commit comments

Comments
 (0)