Skip to content

Commit 8dda38d

Browse files
committed
Merge branch 'streaming' into mobile-app
2 parents d7de818 + 6db5a5a commit 8dda38d

File tree

15 files changed

+724
-116
lines changed

15 files changed

+724
-116
lines changed

apps/mobile/babel.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
module.exports = function (api) {
1+
module.exports = (api) => {
22
api.cache(true);
33
return {
44
presets: [

apps/mobile/global.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
@tailwind base;
22
@tailwind components;
3-
@tailwind utilities;
3+
@tailwind utilities;

apps/mobile/metro.config.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const { getDefaultConfig } = require("expo/metro-config");
2-
const { withNativeWind } = require('nativewind/metro');
3-
4-
const config = getDefaultConfig(__dirname)
5-
6-
module.exports = withNativeWind(config, { input: './global.css' })
2+
const { withNativeWind } = require("nativewind/metro");
3+
4+
const config = getDefaultConfig(__dirname);
5+
6+
module.exports = withNativeWind(config, { input: "./global.css" });

apps/mobile/nativewind-env.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
/// <reference types="nativewind/types" />
1+
/// <reference types="nativewind/types" />

apps/mobile/src/app/chat.tsx

Lines changed: 86 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,94 @@
1-
import { Text, View } from "react-native";
1+
import { useState } from "react";
2+
import {
3+
KeyboardAvoidingView,
4+
Platform,
5+
Text,
6+
TextInput,
7+
TouchableOpacity,
8+
View,
9+
} from "react-native";
210
import { SafeAreaView } from "react-native-safe-area-context";
3-
import { type Message, MessagesList } from "../components/MessagesList";
4-
5-
// Sample messages for demo
6-
const SAMPLE_MESSAGES: Message[] = [
7-
{ id: "1", text: "Hello!" },
8-
{ id: "2", text: "How can I help you today?" },
9-
{ id: "3", text: "Welcome to the chat." },
10-
];
11+
import { MessagesList } from "../components/MessagesList";
12+
import { useMaxStore } from "../stores/maxStore";
1113

1214
export default function ChatScreen() {
15+
const [inputText, setInputText] = useState("");
16+
const { thread, streamingActive, askMax, stopGeneration, resetThread } =
17+
useMaxStore();
18+
19+
const handleSend = async () => {
20+
const trimmed = inputText.trim();
21+
if (!trimmed || streamingActive) return;
22+
23+
setInputText("");
24+
await askMax(trimmed);
25+
};
26+
27+
const handleStop = () => {
28+
stopGeneration();
29+
};
30+
1331
return (
14-
<SafeAreaView className="flex-1 bg-dark-bg">
15-
{/* Header */}
16-
<View className="border-dark-border border-b px-6 pt-4 pb-2">
17-
<Text className="font-bold text-white text-xl">Chat</Text>
18-
</View>
32+
<SafeAreaView
33+
className="flex-1 bg-dark-bg"
34+
edges={["top", "left", "right"]}
35+
>
36+
<KeyboardAvoidingView
37+
behavior={Platform.OS === "ios" ? "padding" : "height"}
38+
className="flex-1"
39+
keyboardVerticalOffset={0}
40+
>
41+
{/* Header */}
42+
<View className="flex-row items-center justify-between border-dark-border border-b px-6 pt-4 pb-2">
43+
<Text className="font-bold text-white text-xl">Max</Text>
44+
{thread.length > 0 && (
45+
<TouchableOpacity onPress={resetThread}>
46+
<Text className="text-blue-500">New chat</Text>
47+
</TouchableOpacity>
48+
)}
49+
</View>
50+
51+
{/* Messages */}
52+
<View className="flex-1">
53+
<MessagesList messages={thread} isLoading={streamingActive} />
54+
</View>
1955

20-
{/* Messages */}
21-
<View className="flex-1">
22-
<MessagesList messages={SAMPLE_MESSAGES} />
23-
</View>
56+
{/* Input area */}
57+
<View className="border-dark-border border-t px-4 py-3">
58+
<View className="flex-row items-end gap-2">
59+
<TextInput
60+
className="max-h-[120px] min-h-[44px] flex-1 rounded-2xl bg-dark-border px-4 py-3 text-base text-white"
61+
placeholder="Ask Max..."
62+
placeholderTextColor="#6B7280"
63+
value={inputText}
64+
onChangeText={setInputText}
65+
onSubmitEditing={handleSend}
66+
multiline
67+
editable={!streamingActive}
68+
returnKeyType="send"
69+
blurOnSubmit={false}
70+
/>
71+
{streamingActive ? (
72+
<TouchableOpacity
73+
onPress={handleStop}
74+
className="h-11 w-11 items-center justify-center rounded-full bg-red-600"
75+
>
76+
<View className="h-4 w-4 rounded-sm bg-white" />
77+
</TouchableOpacity>
78+
) : (
79+
<TouchableOpacity
80+
onPress={handleSend}
81+
disabled={!inputText.trim()}
82+
className={`h-11 w-11 items-center justify-center rounded-full ${
83+
inputText.trim() ? "bg-blue-600" : "bg-dark-border"
84+
}`}
85+
>
86+
<Text className="text-lg text-white"></Text>
87+
</TouchableOpacity>
88+
)}
89+
</View>
90+
</View>
91+
</KeyboardAvoidingView>
2492
</SafeAreaView>
2593
);
2694
}

apps/mobile/src/components/MessagesList.tsx

Lines changed: 81 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,96 @@
1-
import React from 'react';
2-
import { View, Text, FlatList } from 'react-native';
1+
import { ActivityIndicator, FlatList, Text, View } from "react-native";
2+
import {
3+
AssistantMessageType,
4+
isAssistantMessage,
5+
isHumanMessage,
6+
type ThreadMessage,
7+
} from "../types/max";
38

4-
export interface Message {
5-
id: string;
6-
text: string;
9+
interface MessagesListProps {
10+
messages: ThreadMessage[];
11+
isLoading?: boolean;
712
}
813

9-
interface MessagesListProps {
10-
messages: Message[];
14+
function MessageBubble({ message }: { message: ThreadMessage }) {
15+
const isHuman = isHumanMessage(message);
16+
const isAssistant = isAssistantMessage(message);
17+
const isFailure = message.type === AssistantMessageType.Failure;
18+
const isLoading = message.status === "loading";
19+
20+
// Get content based on message type
21+
let content = "";
22+
if (isHuman || isAssistant || isFailure) {
23+
content = message.content || "";
24+
}
25+
26+
// Show thinking indicator for assistant messages
27+
const thinking = isAssistant && message.meta?.thinking?.[0]?.thinking;
28+
29+
return (
30+
<View className={`px-4 py-3 ${isHuman ? "items-end" : "items-start"}`}>
31+
<View
32+
className={`max-w-[85%] rounded-2xl px-4 py-3 ${
33+
isHuman
34+
? "bg-blue-600"
35+
: isFailure
36+
? "bg-red-900/50"
37+
: "bg-dark-border"
38+
}`}
39+
>
40+
{isLoading && !content && thinking ? (
41+
<View className="flex-row items-center gap-2">
42+
<ActivityIndicator size="small" color="#9CA3AF" />
43+
<Text className="text-base text-dark-text-muted italic">
44+
{thinking}
45+
</Text>
46+
</View>
47+
) : isLoading && !content ? (
48+
<View className="flex-row items-center gap-2">
49+
<ActivityIndicator size="small" color="#9CA3AF" />
50+
<Text className="text-base text-dark-text-muted italic">
51+
Thinking...
52+
</Text>
53+
</View>
54+
) : (
55+
<Text
56+
className={`text-base ${isHuman ? "text-white" : isFailure ? "text-red-300" : "text-white"}`}
57+
>
58+
{content}
59+
</Text>
60+
)}
61+
</View>
62+
</View>
63+
);
1164
}
1265

13-
export function MessagesList({ messages }: MessagesListProps) {
66+
export function MessagesList({ messages, isLoading }: MessagesListProps) {
67+
// Add a loading indicator at the end if streaming and last message is complete
68+
const displayMessages = [...messages];
69+
const lastMessage = displayMessages[displayMessages.length - 1];
70+
71+
if (isLoading && (!lastMessage || lastMessage.status === "completed")) {
72+
displayMessages.push({
73+
type: AssistantMessageType.Assistant,
74+
content: "",
75+
status: "loading",
76+
id: "loading-indicator",
77+
});
78+
}
79+
1480
return (
1581
<FlatList
16-
data={messages}
17-
keyExtractor={(item) => item.id}
82+
data={displayMessages}
83+
keyExtractor={(item, index) => item.id || `msg-${index}`}
1884
inverted
19-
renderItem={({ item }) => (
20-
<View className="py-2 px-4">
21-
<Text className="text-white text-base">{item.text}</Text>
22-
</View>
23-
)}
24-
contentContainerStyle={{ flexGrow: 1 }}
85+
renderItem={({ item }) => <MessageBubble message={item} />}
86+
contentContainerStyle={{ flexGrow: 1, justifyContent: "flex-end" }}
2587
ListEmptyComponent={
2688
<View className="flex-1 items-center justify-center">
27-
<Text className="text-dark-text-muted text-base">No messages yet</Text>
89+
<Text className="text-base text-dark-text-muted">
90+
Ask Max anything about your product data
91+
</Text>
2892
</View>
2993
}
3094
/>
3195
);
3296
}
33-
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Text as RNText, TextProps } from 'react-native';
1+
import { Text as RNText, type TextProps } from "react-native";
22

33
export function Text({ className, ...props }: TextProps) {
4-
return <RNText className={`font-mono ${className || ''}`} {...props} />;
4+
return <RNText className={`font-mono ${className || ""}`} {...props} />;
55
}

apps/mobile/src/hooks/useAuth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useAuthStore } from '../stores/authStore';
1+
import { useAuthStore } from "../stores/authStore";
22

33
/**
44
* A convenience hook for accessing common auth state and methods.

apps/mobile/src/lib/secureStorage.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import * as SecureStore from 'expo-secure-store';
2-
import type { StoredTokens } from '../types/oauth';
1+
import * as SecureStore from "expo-secure-store";
2+
import type { StoredTokens } from "../types/oauth";
33

4-
const TOKENS_KEY = 'posthog_oauth_tokens';
4+
const TOKENS_KEY = "posthog_oauth_tokens";
55

66
export async function saveTokens(tokens: StoredTokens): Promise<void> {
77
await SecureStore.setItemAsync(TOKENS_KEY, JSON.stringify(tokens));
@@ -10,7 +10,7 @@ export async function saveTokens(tokens: StoredTokens): Promise<void> {
1010
export async function getTokens(): Promise<StoredTokens | null> {
1111
const value = await SecureStore.getItemAsync(TOKENS_KEY);
1212
if (!value) return null;
13-
13+
1414
try {
1515
return JSON.parse(value) as StoredTokens;
1616
} catch {

0 commit comments

Comments
 (0)