Skip to content

Commit 4432af7

Browse files
committed
feat: Add weather tool UI and refactor drawer context to automatically close on thread selection.
1 parent 3db45bd commit 4432af7

8 files changed

Lines changed: 206 additions & 63 deletions

File tree

apps/mobile/.expo/devices.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"devices": [
33
{
44
"installationId": "FF1011A1-7709-4E6F-AF64-252B30FCADD0",
5-
"lastUsed": 1771013690224
5+
"lastUsed": 1771017602524
66
}
77
]
88
}

apps/mobile/app/(drawer)/index.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import React from "react";
22
import { Header } from "~/components/header";
33
import { View, KeyboardAvoidingView, Platform } from "react-native";
4-
import { DrawerButton } from "~/components/drawer-button";
54
import { ModelSwitch } from "~/components/model-switch";
65
import { Thread } from "~/components/ai-chat/thread";
6+
import { OpenDrawerButton } from "~/components/context/drawer-context";
77

88
const HomeScreen = () => {
99
const rightComponents = [<ModelSwitch key="model-switch" />];
10-
11-
const leftComponent = [<DrawerButton key="drawer-button" />];
10+
const leftComponent = [<OpenDrawerButton key="drawer-button" />];
1211

1312
return (
1413
<View className="relative flex-1 bg-background">

apps/mobile/components/ai-chat/components-provider.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,17 @@ export const appNativeComponents = {
1717
Form: View,
1818
Button: ({
1919
onClick,
20+
onPress,
2021
...props
2122
}: React.ComponentProps<RuntimeComponents["Button"]> &
22-
Omit<React.ComponentProps<typeof Button>, "onPress">) => (
23-
<Button onPress={onClick} {...props}>
23+
React.ComponentProps<typeof Button>) => (
24+
<Button
25+
onPress={(e) => {
26+
onPress?.(e);
27+
onClick?.(e);
28+
}}
29+
{...props}
30+
>
2431
{props.children}
2532
</Button>
2633
),

apps/mobile/components/ai-chat/thread-list.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import {
99
import * as ThreadListPrimitive from "@creatorem/ai-react-native/primitives/thread-list";
1010
import * as ThreadListItemPrimitive from "@creatorem/ai-react-native/primitives/thread-list-item";
1111
import { useThreads } from "@creatorem/ai-react-native/ai-provider";
12-
import { useCallback, type FC } from "react";
12+
import { type FC } from "react";
1313
import { useCSSVariable } from "uniwind";
1414
import { Icon } from "~/components/ui/icon";
15+
import { useDrawer } from "~/components/context/drawer-context";
1516

1617
export const ThreadList: FC = () => {
1718
const isLoading = useThreads((threads) => threads.isLoading);
@@ -28,10 +29,14 @@ export const ThreadList: FC = () => {
2829
};
2930

3031
const ThreadListNew: FC = () => {
32+
const { setOpen: setDrawerOpen } = useDrawer();
3133
return (
3234
<ThreadListPrimitive.New
3335
variant="outline"
3436
className="mb-4 h-9 justify-start gap-2 rounded-lg px-3 text-sm hover:bg-muted data-active:bg-muted"
37+
onPress={() => {
38+
setDrawerOpen(false);
39+
}}
3540
>
3641
<Icon name="Plus" className="size-4" />
3742
<Text>New Thread</Text>
@@ -58,9 +63,16 @@ const ThreadListSkeleton: FC = () => {
5863
};
5964

6065
const ThreadListItem: FC = () => {
66+
const { setOpen: setDrawerOpen } = useDrawer();
67+
6168
return (
6269
<ThreadListItemPrimitive.Root className="group flex h-9 flex-row items-center gap-2 rounded-lg transition-colors hover:bg-muted focus-visible:bg-muted focus-visible:outline-none data-active:bg-muted">
63-
<ThreadListItemPrimitive.Trigger className="flex h-full min-w-0 flex-1 items-center justify-start truncate bg-transparent px-3 text-start text-sm">
70+
<ThreadListItemPrimitive.Trigger
71+
onPress={() => {
72+
setDrawerOpen(false);
73+
}}
74+
className="flex h-full min-w-0 flex-1 items-center justify-start truncate bg-transparent px-3 text-start text-sm"
75+
>
6476
<ThreadListItemPrimitive.Title
6577
fallback="New Chat"
6678
className="text-foreground"

apps/mobile/components/ai-chat/thread.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@ import { ToolFallback } from "./tool-fallback";
2323
import { MarkdownText } from "./markdown-text";
2424
import { useCSSVariable } from "uniwind";
2525
import { ComposerAddAttachment } from "./attachment";
26+
import { WeatherToolRegistration } from "../tools/weather-tool-ui";
2627

2728
export const Thread: React.FC = () => {
2829
return (
2930
<ThreadPrimitive.Root>
31+
<WeatherToolRegistration />
3032
<View className="flex-1">
3133
<ThreadPrimitive.Viewport turnAnchor="top">
3234
<ThreadPrimitive.ViewportScrollable
@@ -282,7 +284,7 @@ const AssistantActionBar: FC = () => {
282284
hideWhenRunning
283285
autohide="not-last"
284286
autohideFloat="single-branch"
285-
className="col-start-3 row-start-2 -ml-1 flex flex-row gap-1 text-muted-foreground data-floating:absolute data-floating:rounded-md data-floating:border data-floating:bg-background data-floating:p-1 data-floating:shadow-sm"
287+
className="col-start-3 row-start-2 -ml-1 flex flex-row gap-1 text-muted-foreground"
286288
>
287289
<StaggerFadeInItem index={0}>
288290
<ActionBarPrimitive.Copy size="sm-icon" variant="ghost">
Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,31 @@
1-
import React, { createContext, useContext, useCallback } from "react";
2-
import { DrawerActions } from "@react-navigation/native";
3-
import { useNavigation } from "expo-router";
1+
import React, {
2+
createContext,
3+
useContext,
4+
useCallback,
5+
useState,
6+
useEffect,
7+
} from "react";
8+
import { Pressable, View } from "react-native";
9+
import { Icon } from "~/components/ui/icon";
10+
import {
11+
DrawerActions,
12+
useNavigation,
13+
NavigationProp,
14+
} from "@react-navigation/native";
15+
import { useCSSVariable } from "uniwind";
416

517
interface DrawerContextType {
6-
openDrawer: () => void;
7-
closeDrawer: () => void;
8-
toggleDrawer: () => void;
18+
open: boolean;
19+
setOpen: (open: boolean) => void;
920
}
1021

1122
const DrawerContext = createContext<DrawerContextType | undefined>(undefined);
1223

1324
export function DrawerProvider({ children }: { children: React.ReactNode }) {
14-
const navigation = useNavigation();
15-
16-
const openDrawer = useCallback(() => {
17-
navigation.dispatch(DrawerActions.openDrawer());
18-
}, [navigation]);
19-
20-
const closeDrawer = useCallback(() => {
21-
navigation.dispatch(DrawerActions.closeDrawer());
22-
}, [navigation]);
23-
24-
const toggleDrawer = useCallback(() => {
25-
navigation.dispatch(DrawerActions.toggleDrawer());
26-
}, [navigation]);
25+
const [open, setOpen] = useState(false);
2726

2827
return (
29-
<DrawerContext.Provider value={{ openDrawer, closeDrawer, toggleDrawer }}>
28+
<DrawerContext.Provider value={{ open, setOpen }}>
3029
{children}
3130
</DrawerContext.Provider>
3231
);
@@ -42,3 +41,32 @@ export function useDrawer() {
4241
return context;
4342
}
4443

44+
export const OpenDrawerButton = () => {
45+
const { open, setOpen } = useDrawer();
46+
const textColor = useCSSVariable("--color-foreground");
47+
const navigation = useNavigation<NavigationProp<any>>();
48+
49+
const openDrawer = useCallback(() => {
50+
setOpen(true);
51+
}, [setOpen]);
52+
53+
useEffect(() => {
54+
if (open) {
55+
navigation.dispatch(DrawerActions.openDrawer());
56+
} else {
57+
navigation.dispatch(DrawerActions.closeDrawer());
58+
}
59+
}, [open, navigation.dispatch]);
60+
61+
return (
62+
<View className={`rounded-full`}>
63+
<Pressable
64+
onPress={openDrawer}
65+
style={({ pressed }) => [{ opacity: pressed ? 0.7 : 1 }]}
66+
className="rounded-full border border-border bg-background p-3"
67+
>
68+
<Icon name="Menu" size={24} color={textColor} />
69+
</Pressable>
70+
</View>
71+
);
72+
};

apps/mobile/components/drawer-button.tsx

Lines changed: 0 additions & 34 deletions
This file was deleted.
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { FC, useMemo } from "react";
2+
import { View } from "react-native";
3+
import { useAssistantToolUI } from "@creatorem/ai-chat";
4+
import type { ToolCallMessagePartProps } from "@creatorem/ai-chat/types/message-part-component-types";
5+
import { Text } from "../ui/text";
6+
import { cn } from "~/utils/cn";
7+
8+
const HOUR_LABELS = ["1PM", "2PM", "3PM", "4PM", "5PM"] as const;
9+
10+
const toCelsius = (fahrenheit: number) =>
11+
Math.round(((fahrenheit ?? 0) - 32) * (5 / 9));
12+
13+
const getConditionEmoji = (condition?: string) => {
14+
const value = (condition ?? "").toLowerCase();
15+
if (value.includes("rain") || value.includes("storm")) return "🌧️";
16+
if (value.includes("snow") || value.includes("sleet")) return "❄️";
17+
if (value.includes("cloud") || value.includes("overcast")) return "☁️";
18+
if (value.includes("fog")) return "🌫️";
19+
if (value.includes("wind")) return "💨";
20+
return "☀️";
21+
};
22+
23+
type ForecastDay = {
24+
date: string;
25+
temperatureF: number;
26+
condition: string;
27+
sunlightHours: number;
28+
};
29+
30+
const WeatherToolCard: FC<ToolCallMessagePartProps> = ({
31+
args,
32+
result,
33+
status,
34+
}) => {
35+
const forecast = (result as any)?.forecast as ForecastDay[] | undefined;
36+
const location = (result as any)?.location ?? (args as any)?.location ?? "—";
37+
const range = (result as any)?.range as
38+
| { startDate: string; endDate: string }
39+
| undefined;
40+
41+
const temps = forecast?.map((day) => day.temperatureF) ?? [];
42+
const highF = temps.length ? Math.max(...temps) : undefined;
43+
const lowF = temps.length ? Math.min(...temps) : undefined;
44+
const currentF = forecast?.[0]?.temperatureF;
45+
const currentCondition = forecast?.[0]?.condition;
46+
47+
const hourly = useMemo(
48+
() =>
49+
forecast
50+
? forecast.slice(0, HOUR_LABELS.length).map((day, index) => ({
51+
label: HOUR_LABELS[index],
52+
tempC: toCelsius(day.temperatureF),
53+
condition: day.condition,
54+
}))
55+
: [],
56+
[forecast],
57+
);
58+
59+
return (
60+
<View className="mb-4 w-full rounded-3xl border border-sky-200/50 bg-sky-500 p-4">
61+
<View className="flex-row items-start justify-between">
62+
<View className="flex-row items-center gap-3">
63+
<View className="h-12 w-12 items-center justify-center rounded-full bg-white/20">
64+
<Text className="text-2xl">
65+
{getConditionEmoji(currentCondition)}
66+
</Text>
67+
</View>
68+
<View>
69+
<Text className="font-semibold text-3xl text-white">
70+
{currentF !== undefined ? `${toCelsius(currentF)}°C` : "--"}
71+
</Text>
72+
<Text className="text-sm text-white/85">{location}</Text>
73+
</View>
74+
</View>
75+
<View>
76+
<Text className="text-right text-sm text-white/85">
77+
H:{highF !== undefined ? `${toCelsius(highF)}°` : "--"}
78+
</Text>
79+
<Text className="text-right text-sm text-white/85">
80+
L:{lowF !== undefined ? `${toCelsius(lowF)}°` : "--"}
81+
</Text>
82+
</View>
83+
</View>
84+
85+
{range && (
86+
<Text className="mt-1 text-white/75 text-xs">
87+
{range.startDate} - {range.endDate}
88+
</Text>
89+
)}
90+
91+
{!forecast && (
92+
<Text className="mt-3 text-sm text-white/85">
93+
{status.type === "running"
94+
? "Fetching forecast..."
95+
: "No forecast yet."}
96+
</Text>
97+
)}
98+
99+
{forecast && (
100+
<View className="mt-4 flex-row justify-between gap-2">
101+
{hourly.map((slot) => (
102+
<View
103+
key={slot.label}
104+
className={cn(
105+
"min-w-[58px] flex-1 items-center rounded-2xl bg-white/15 px-2 py-3",
106+
)}
107+
>
108+
<Text className="text-white/80 text-xs">{slot.label}</Text>
109+
<Text className="my-1 text-xl">
110+
{getConditionEmoji(slot.condition)}
111+
</Text>
112+
<Text className="font-medium text-sm text-white">
113+
{slot.tempC}°C
114+
</Text>
115+
</View>
116+
))}
117+
</View>
118+
)}
119+
</View>
120+
);
121+
};
122+
123+
export const WeatherToolRegistration: FC = () => {
124+
useAssistantToolUI({
125+
toolName: "weather",
126+
render: WeatherToolCard,
127+
});
128+
return null;
129+
};

0 commit comments

Comments
 (0)