Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement an AI Advisor #125

Merged
merged 33 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
dc0c986
add Bot icon
scottchen98 Nov 16, 2024
82dec77
add AI Advisor tab with Bot icon
scottchen98 Nov 16, 2024
b28f217
implement AdvisorChatbot with message input and scrolling functionality
scottchen98 Nov 16, 2024
1d7c32b
add modern-async and openai packages to dependencies
scottchen98 Nov 16, 2024
b8b3888
add messages and threads tables to schema
scottchen98 Nov 19, 2024
56e8fda
add messages and serve modules to API types
scottchen98 Nov 19, 2024
cffc285
add message handling functions for listing, sending, and clearing mes…
scottchen98 Nov 19, 2024
68fc15f
implement OpenAI thread management and message handling in serve module
scottchen98 Nov 19, 2024
5e58dbc
implement session management and message handling in AdvisorChatbot c…
scottchen98 Nov 19, 2024
6880977
temporary commenting out message handling logic and adding static mes…
scottchen98 Nov 19, 2024
3eae261
Update styling:
michaeleii Nov 20, 2024
98d47da
Use view instead of safearea view
michaeleii Nov 20, 2024
1bc06d8
Edit styling
michaeleii Nov 20, 2024
f181cdb
Edit font
michaeleii Nov 20, 2024
56a425f
Refactor AdvisorChatbot component: restore message handling logic and…
scottchen98 Nov 20, 2024
0f9b951
Replace View with SafeAreaView for improved layout handling
scottchen98 Nov 21, 2024
2745e06
Merge branch 'expo-sdk-52' of https://github.com/Vero-Ventures/live-t…
scottchen98 Nov 21, 2024
4659b7d
Finish rewards page
michaeleii Dec 6, 2024
95c970e
Add single rewards page
michaeleii Dec 6, 2024
f110724
Finish single rewards page
michaeleii Dec 6, 2024
d4852d5
Merge branch 'sdk-52' into 69-integrate-with-gift-card-provider-api
michaeleii Dec 6, 2024
e04bdb5
Refactor AdvisorChatbot to use threads for message handling and updat…
scottchen98 Dec 6, 2024
f9cf720
Merge branch 'main' of https://github.com/Vero-Ventures/live-timeless…
scottchen98 Dec 6, 2024
ed4cd95
Add legal disclosure to gift cards view
michaeleii Dec 6, 2024
209fa4f
Rename to dom content to for clarity
michaeleii Dec 6, 2024
00d48c1
Merge branch '69-integrate-with-gift-card-provider-api' of https://gi…
scottchen98 Dec 6, 2024
f78f9c7
fix merge conflict
scottchen98 Dec 6, 2024
d042296
Get user id for thread creation
scottchen98 Dec 6, 2024
feca416
Add message handling for user and assistant roles in the database
scottchen98 Dec 6, 2024
43198fd
Refactor input and button layout in AdvisorChatbot for improved styli…
scottchen98 Dec 10, 2024
964f495
Update placeholder text and enhance message component layout in Advis…
scottchen98 Dec 10, 2024
93b4e98
update styling
scottchen98 Dec 10, 2024
8f1bc7c
reinstall packages
scottchen98 Dec 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { User } from "~/lib/icons/User";
import { Goal, Mountain, TrendingUp } from "lucide-react-native";
import { fontFamily } from "~/lib/font";
import { Star } from "~/lib/icons/Star";
import { Bot } from "~/lib/icons/Bot";

export default function TabLayout() {
return (
Expand Down Expand Up @@ -83,6 +84,18 @@ export default function TabLayout() {
),
}}
/>
<Tabs.Screen
name="ai-advisor"
options={{
title: "Advisor",
headerShown: false,
tabBarIcon: ({ color }) => (
<View style={{ alignItems: "center" }}>
<Bot color={color} />
</View>
),
}}
/>
</Tabs>
</>
);
Expand Down
100 changes: 100 additions & 0 deletions app/(tabs)/ai-advisor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { SafeAreaView } from "react-native-safe-area-context";
// import AsyncStorage from "@react-native-async-storage/async-storage";

import { useState, useRef, useMemo } from "react";
import { View, KeyboardAvoidingView, Platform, FlatList } from "react-native";
import { Text } from "~/components/ui/text";
import { Button } from "~/components/ui/button";
import { useMutation, useQuery } from "convex/react";
import { api } from "~/convex/_generated/api";
import { cn } from "~/lib/utils";
import { Send } from "~/lib/icons/Send";
import { Input } from "~/components/ui/input";
import { Separator } from "~/components/ui/separator";

export default function AdvisorChatbot() {
const thread = useQuery(api.threads.getThread);
const remoteMessages = useQuery(api.messages.getMessages, {
threadId: thread?.threadId,
});
const sendMessage = useMutation(api.messages.sendMessage);

const messages = useMemo(
() =>
[
{
role: "assistant",
content:
"Hey there, I'm your personal AI Advisor. What can I help you with?",
threadId: "",
},
].concat(remoteMessages ?? []),
[remoteMessages]
);

const [inputText, setInputText] = useState("");
const flatListRef = useRef<FlatList>(null);

const handleSend = async () => {
if (!inputText) {
return;
}
await sendMessage({ threadId: thread?.threadId, message: inputText });
setInputText("");
};

return (
<SafeAreaView
style={{ flex: 1, backgroundColor: "#082139" }}
edges={["top", "left", "right"]}
>
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
className="flex-1"
>
<FlatList
ref={flatListRef}
data={messages}
onContentSizeChange={() => {
if (flatListRef.current) {
flatListRef.current.scrollToEnd({ animated: true });
}
}}
renderItem={({ item }) => (
<MessageComp isViewer={item.role === "user"} text={item.content} />
)}
ItemSeparatorComponent={() => <Separator />}
/>

<View className="relative flex-row items-center justify-between bg-card">
<Input
className="native:h-20 flex-1 rounded-none border-0 bg-card py-6 placeholder:text-muted-foreground"
value={inputText}
onChangeText={setInputText}
placeholder="Send a message..."
multiline
/>
<Button variant="ghost" hitSlop={20} onPress={handleSend}>
<Send className="text-white" />
</Button>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
);
}

function MessageComp({ text, isViewer }: { text: string; isViewer: boolean }) {
return (
<View className={cn("gap-3 p-4", isViewer && "bg-card")}>
<Text className={cn(!isViewer && "font-semibold")}>{text}</Text>
<Text
className={cn(
"text-sm font-light text-muted-foreground",
isViewer && "text-right"
)}
>
{isViewer ? "You" : "LT AI Advisor"}
</Text>
</View>
);
}
197 changes: 80 additions & 117 deletions app/(tabs)/rewards.tsx
Original file line number Diff line number Diff line change
@@ -1,151 +1,114 @@
import { ActivityIndicator, FlatList, Pressable, View } from "react-native";
import type { LucideIcon } from "lucide-react-native";
import {
FlatList,
Pressable,
View,
Image,
ActivityIndicator,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";

import { Link } from "expo-router";
import { Link, useLocalSearchParams } from "expo-router";
import { Text } from "~/components/ui/text";
import SearchInput from "~/components/search-input";
import { Coins } from "~/lib/icons/Coins";
import { HandHeart } from "~/lib/icons/HandHeart";
import { Gift } from "~/lib/icons/Gift";
import { TentTree } from "~/lib/icons/TentTree";
import { api } from "~/convex/_generated/api";
import { useQuery } from "convex/react";
import { useAction, useQuery } from "convex/react";
import { useEffect, useState } from "react";
import type { ListProductsResponseProductsInner } from "tremendous";
import { MaterialIcons } from "@expo/vector-icons";

export default function RewardsPage() {
const [rewards, setRewards] = useState<
ListProductsResponseProductsInner[] | null
>(null);
const { query } = useLocalSearchParams<{ query?: string }>();
const user = useQuery(api.users.currentUser);
const fetchRewards = useAction(api.tremendous.listRewardsAction);

useEffect(() => {
fetchRewards().then((products) => setRewards(products));
}, [fetchRewards]);

const filteredRewards = rewards
? query
? rewards.filter((reward) =>
reward.name.toLowerCase().includes(query.toLowerCase())
)
: rewards
: null;

return (
<SafeAreaView style={{ flex: 1, backgroundColor: "#082139" }}>
{user ? (
<View>
<View className="gap-10 p-4">
<View className="flex flex-row items-center gap-4">
<Text>
<Coins className="text-primary" size={40} />
</Text>
<View className="gap-2">
<Text className="text-2xl font-bold">{user?.points ?? 0}</Text>
<Text className="text-md">LT Token Balance</Text>
</View>
</View>
<View className="gap-3">
<Text className="text-2xl font-semibold">Available Rewards</Text>
<SearchInput />
</View>
<View className="gap-10 bg-background p-4">
<View className="flex flex-row items-center gap-4">
<Text>
<Coins className="text-primary" size={40} />
</Text>
<View className="gap-2">
<Text className="text-2xl font-bold">{user?.points ?? 0}</Text>
<Text className="text-md">LT Token Balance</Text>
</View>
<FlatList
contentContainerStyle={{
paddingBottom: 208,
}}
data={rewardData}
ItemSeparatorComponent={() => <View className="py-2" />}
ListFooterComponent={() => (
<Text className="my-5 text-center text-sm">End of Rewards</Text>
)}
renderItem={({ item }) => (
<RewardItem
id={item.id}
Icon={item.icon}
type={item.type}
token={item.token}
name={item.name}
description={item.description}
/>
)}
/>
</View>
<View className="gap-3">
<Text className="text-2xl font-semibold">Available Rewards</Text>
<SearchInput query={query} />
</View>
</View>
{filteredRewards ? (
<FlatList
data={filteredRewards}
ItemSeparatorComponent={() => <View className="py-2" />}
renderItem={({ item }) => <RewardItem product={item} />}
/>
) : (
<View className="flex-1 bg-background">
<View className="h-full flex-1 items-center justify-center bg-background">
<ActivityIndicator />
</View>
)}
</SafeAreaView>
);
}

export const rewardData = [
{
id: "0",
icon: HandHeart,
type: "Charitable donation",
token: 30,
name: "Reward Name",
description: "This is a short description for this reward",
},
{
id: "1",
icon: Gift,
type: "Gift card",
token: 20,
name: "Reward Name",
description: "This is a short description for this reward",
},
{
id: "2",
icon: TentTree,
type: "Experience",
token: 50,
name: "Reward Name",
description: "This is a short description for this reward",
},
{
id: "3",
icon: TentTree,
type: "Experience",
token: 40,
name: "Reward Name",
description: "This is a short description for this reward",
},
{
id: "4",
icon: Gift,
type: "Gift card",
token: 60,
name: "Reward Name",
description: "This is a short description for this reward",
},
];
interface RewardItemProps {
product: ListProductsResponseProductsInner;
}

function RewardItem({
id,
Icon,
type,
token,
name,
description,
}: {
id: string;
Icon: LucideIcon;
type: string;
token: number;
name: string;
description: string;
}) {
function RewardItem({ product }: RewardItemProps) {
return (
<Link
href={{
pathname: "/rewards/[id]",
params: { id },
params: { id: product.id },
}}
asChild
>
<Pressable>
<View className="justify-between bg-[#0e2942] px-6 py-5">
<View className="flex flex-row justify-between">
<View className="flex flex-row items-center gap-2">
<View className="rounded-lg bg-white/20 p-1 backdrop-blur-sm">
<Icon className="text-foreground" />
</View>
<Text>{type}</Text>
</View>
<Text className="font-medium">{token} tokens</Text>
</View>
<View className="gap-1">
<Text className="text-xl font-semibold">{name}</Text>
<Text className="line-clamp-2 overflow-ellipsis">
{description}
</Text>
<View className="relative">
<View className="absolute h-full w-full bg-[#0e2942]/50"></View>
{!!product.images.length && (
<Image
src={product.images.at(0)?.src}
className="-z-10 h-[200px] w-full"
/>
)}

<View className="absolute flex flex-row items-center gap-2 pl-4 pt-4">
{product.category === "merchant_card" && (
<>
<View className="rounded-lg bg-slate-500 p-1">
<MaterialIcons
name="card-giftcard"
size={20}
color={"white"}
/>
</View>
<Text>Gift Card</Text>
</>
)}
</View>
<Text className="absolute bottom-0 pb-4 pl-4 text-xl font-semibold">
{product.name}
</Text>
</View>
</Pressable>
</Link>
Expand Down
Loading
Loading