Skip to content

Commit 0177ad3

Browse files
authored
Merge pull request #12 from BYLinMou/feat/overall-enhance-3
Feat/overall enhance 3
2 parents 6beeff8 + 1a03e46 commit 0177ad3

File tree

12 files changed

+403
-117
lines changed

12 files changed

+403
-117
lines changed

README.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ AuraSpend is a gamified, AI-powered expense tracking application built with Reac
2525
- **Level Up**: Gain XP by logging transactions and staying within budget.
2626
- **Rewards**: Unlock outfits and new pets as you progress.
2727

28-
### 🤖 AI Assistant
28+
### 🤖 AI Agent (Aura Assistant)
2929
- **Floating Chat**: An integrated AI assistant that can answer questions about your spending, summarize data, and perform actions via natural language (e.g., "How much did I spend on food last week?").
3030

3131
### 🛠 Technical Highlights
@@ -39,23 +39,30 @@ AuraSpend is a gamified, AI-powered expense tracking application built with Reac
3939

4040
- **Frontend**: React Native, Expo, Expo Router, Reanimated
4141
- **Backend**: Supabase (PostgreSQL, Auth, Realtime, Storage)
42-
- **AI Integration**: OpenAI, Gemini API
42+
- **AI Integration**: OpenAI-compatible APIs
4343
- **Language**: TypeScript
4444

4545
---
4646

4747
## 🚀 Getting Started
4848

49+
### Receipt Extraction Model Testing (Optional)
50+
You can use the Python tool in [`TestTools/ReceiptSmartAnalyzer`](TestTools/ReceiptSmartAnalyzer) to test and evaluate receipt information extraction capabilities. See its README for usage instructions.
51+
52+
### Supabase Setup (Optional)
53+
If you want to set up your own Supabase backend, see [`supabase/README.md`](supabase/README.md) for step-by-step instructions and example SQL to initialize your database.
54+
4955
### Prerequisites
5056
- Node.js (LTS version recommended, e.g., >= 20)
5157
- npm or yarn
5258
- Expo Go app on your mobile device or an Android/iOS Simulator
59+
- Android users: you can also download the latest APK directly from the Releases page: [https://github.com/BYLinMou/COMP3330-Gp20-AuraSpend/releases/latest](https://github.com/BYLinMou/COMP3330-Gp20-AuraSpend/releases/latest)
5360

5461
### 1. Clone and Install
5562

5663
```bash
5764
# Clone the repository
58-
git clone [https://github.com/BYLinMou/COMP3330-Gp20-AuraSpend.git](https://github.com/BYLinMou/COMP3330-Gp20-AuraSpend.git)
65+
git clone https://github.com/BYLinMou/COMP3330-Gp20-AuraSpend.git
5966

6067
# Navigate to project folder
6168
cd COMP3330-Gp20-AuraSpend
@@ -65,5 +72,5 @@ cp .env.example .env
6572
# Install dependencies
6673
npm install
6774

68-
#Start Project
75+
# Start Project
6976
npm run start

app.config.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export default ({ config }) => {
1111
console.warn('Please create a .env file with EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_ANON_KEY');
1212
}
1313

14-
const appVersion = "0.0.10";
14+
const appVersion = "0.0.11";
1515

1616
return {
1717
expo: {
@@ -27,13 +27,14 @@ export default ({ config }) => {
2727
supportsTablet: true
2828
},
2929
android: {
30-
adaptiveIcon: {
31-
backgroundColor: "#E6F4FE",
32-
// foregroundImage: "./assets/images/android-icon-foreground.png",
33-
foregroundImage: "./assets/images/icon.png",
34-
backgroundImage: "./assets/images/android-icon-background.png",
35-
monochromeImage: "./assets/images/android-icon-monochrome.png"
36-
},
30+
icon: "./assets/images/icon.png",
31+
// adaptiveIcon: {
32+
// backgroundColor: "#E6F4FE",
33+
// // foregroundImage: "./assets/images/android-icon-foreground.png",
34+
// foregroundImage: "./assets/images/icon.png",
35+
// backgroundImage: "./assets/images/android-icon-background.png",
36+
// monochromeImage: "./assets/images/android-icon-monochrome.png"
37+
// },
3738
edgeToEdgeEnabled: true,
3839
predictiveBackGestureEnabled: false,
3940
permissions: [],

app/(tabs)/index.tsx

Lines changed: 74 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useState, useEffect } from 'react';
2-
import { View, Text, StyleSheet, Dimensions, ActivityIndicator, TouchableOpacity, Modal, Alert, Pressable, Animated } from 'react-native';
2+
import { View, Text, StyleSheet, Dimensions, ActivityIndicator, TouchableOpacity, Modal, Alert, Pressable, Animated, Switch, ScrollView } from 'react-native';
33
import { SafeAreaView } from 'react-native-safe-area-context';
44
import { LinearGradient } from 'expo-linear-gradient';
55
import { Ionicons } from '@expo/vector-icons';
@@ -59,6 +59,7 @@ export default function HomeScreen() {
5959
const [isFlipped, setIsFlipped] = useState(false);
6060
const [paymentMethodBalances, setPaymentMethodBalances] = useState<Record<string, number>>({});
6161
const [loadingPaymentMethods, setLoadingPaymentMethods] = useState(false);
62+
const [hideZeroBalances, setHideZeroBalances] = useState(true);
6263
const [showBackSide, setShowBackSide] = useState(false);
6364
const [showCardContent, setShowCardContent] = useState(true);
6465
const blurAnimRef = React.useRef<{ [key: string]: Animated.Value }>({});
@@ -322,8 +323,8 @@ export default function HomeScreen() {
322323
onRefresh={onRefresh}
323324
>
324325
{/* Balance Card */}
325-
<TouchableOpacity onPress={handleFlip} activeOpacity={0.9}>
326-
<View style={styles.flipContainer}>
326+
<View style={styles.flipContainer}>
327+
<View>
327328
{/* Front Side */}
328329
<Animated.View
329330
style={[
@@ -346,28 +347,30 @@ export default function HomeScreen() {
346347
]}
347348
pointerEvents={isFlipped ? 'none' : 'auto'}
348349
>
349-
<LinearGradient
350-
colors={[Colors.gradientStart1, Colors.gradientEnd1]}
351-
style={styles.balanceCard}
352-
start={{ x: 0, y: 0 }}
353-
end={{ x: 1, y: 1 }}
354-
>
355-
<View style={styles.balanceHeader}>
356-
<Text style={styles.balanceLabel}>{t('home.remainingBudget')}</Text>
357-
<Ionicons name="wallet-outline" size={24} color={Colors.white} />
358-
</View>
359-
<Text style={styles.balanceAmount}>{currencySymbol}{balance.toFixed(2)}</Text>
360-
<View style={styles.balanceFooter}>
361-
<View style={styles.balanceItem}>
362-
<Ionicons name="wallet" size={16} color={Colors.white} />
363-
<Text style={styles.balanceItemText}>{t('home.budget')}: {currencySymbol}{budget.toFixed(2)}</Text>
350+
<TouchableOpacity activeOpacity={1} onPress={handleFlip}>
351+
<LinearGradient
352+
colors={[Colors.gradientStart1, Colors.gradientEnd1]}
353+
style={styles.balanceCard}
354+
start={{ x: 0, y: 0 }}
355+
end={{ x: 1, y: 1 }}
356+
>
357+
<View style={styles.balanceHeader}>
358+
<Text style={styles.balanceLabel}>{t('home.remainingBudget')}</Text>
359+
<Ionicons name="wallet-outline" size={24} color={Colors.white} />
364360
</View>
365-
<View style={styles.balanceItem}>
366-
<Ionicons name="trending-down" size={16} color={Colors.white} />
367-
<Text style={styles.balanceItemText}>{t('home.spent')}: {currencySymbol}{spent.toFixed(2)}</Text>
361+
<Text style={styles.balanceAmount}>{currencySymbol}{balance.toFixed(2)}</Text>
362+
<View style={styles.balanceFooter}>
363+
<View style={styles.balanceItem}>
364+
<Ionicons name="wallet" size={16} color={Colors.white} />
365+
<Text style={styles.balanceItemText}>{t('home.budget')}: {currencySymbol}{budget.toFixed(2)}</Text>
366+
</View>
367+
<View style={styles.balanceItem}>
368+
<Ionicons name="trending-down" size={16} color={Colors.white} />
369+
<Text style={styles.balanceItemText}>{t('home.spent')}: {currencySymbol}{spent.toFixed(2)}</Text>
370+
</View>
368371
</View>
369-
</View>
370-
</LinearGradient>
372+
</LinearGradient>
373+
</TouchableOpacity>
371374
</Animated.View>
372375

373376
{/* Back Side - Only render when visible to avoid blocking */}
@@ -400,23 +403,42 @@ export default function HomeScreen() {
400403
start={{ x: 0, y: 0 }}
401404
end={{ x: 1, y: 1 }}
402405
>
403-
<View style={styles.balanceHeader}>
404-
<Text style={styles.balanceLabel}>Payment Method Balances</Text>
405-
<Ionicons name="card-outline" size={24} color={Colors.white} />
406-
</View>
406+
<TouchableOpacity activeOpacity={1} onPress={handleFlip}>
407+
<View style={styles.balanceHeader}>
408+
<Text style={styles.balanceLabel}>{t('home.paymentMethodBalances')}</Text>
409+
<View style={styles.paymentMethodHeaderRight}>
410+
<Text style={styles.hideZeroLabel}>{t('home.hideZeroBalances') || 'Hide 0'}</Text>
411+
<Switch
412+
value={hideZeroBalances}
413+
onValueChange={setHideZeroBalances}
414+
trackColor={{ false: Colors.gray200, true: Colors.primary }}
415+
thumbColor={hideZeroBalances ? Colors.white : Colors.white}
416+
/>
417+
</View>
418+
</View>
419+
</TouchableOpacity>
407420

408421
{loadingPaymentMethods ? (
409422
<View style={styles.paymentMethodsLoading}>
410423
<ActivityIndicator size="large" color={Colors.white} />
411424
</View>
412425
) : (
413-
<View style={styles.paymentMethodsList}>
414-
{Object.entries(paymentMethodBalances).length === 0 ? (
415-
<Text style={styles.paymentMethodEmpty}>{t('home.noPaymentMethods')}</Text>
416-
) : (
417-
Object.entries(paymentMethodBalances)
418-
.sort(([, a], [, b]) => Math.abs(b) - Math.abs(a))
419-
.map(([method, balance]) => {
426+
<ScrollView
427+
style={[styles.paymentMethodsScrollView, { maxHeight: Dimensions.get('window').height * 0.7 }]}
428+
contentContainerStyle={styles.paymentMethodsList}
429+
showsVerticalScrollIndicator={true}
430+
nestedScrollEnabled={true}
431+
onStartShouldSetResponder={() => true}
432+
onMoveShouldSetResponder={() => true}
433+
>
434+
{(() => {
435+
const visibleEntries = Object.entries(paymentMethodBalances)
436+
.filter(([, b]) => (!hideZeroBalances || Math.abs(b) > 0))
437+
.sort(([, a], [, b]) => Math.abs(b) - Math.abs(a));
438+
if (visibleEntries.length === 0) {
439+
return <Text style={styles.paymentMethodEmpty}>{t('home.noPaymentMethods')}</Text>;
440+
}
441+
return visibleEntries.map(([method, balance]) => {
420442
const icon =
421443
method.toLowerCase().includes('cash') ? 'cash-outline' :
422444
method.toLowerCase().includes('octopus') ? 'card-outline' :
@@ -441,14 +463,14 @@ export default function HomeScreen() {
441463
</View>
442464
);
443465
})
444-
)}
445-
</View>
466+
})()}
467+
</ScrollView>
446468
)}
447469
</LinearGradient>
448470
</Animated.View>
449471
)}
450472
</View>
451-
</TouchableOpacity>
473+
</View>
452474

453475
{/* Monthly Budget */}
454476
{!isFlipped && showCardContent && (
@@ -1277,10 +1299,24 @@ const styles = StyleSheet.create({
12771299
alignItems: 'center',
12781300
justifyContent: 'center',
12791301
},
1280-
paymentMethodsList: {
1302+
paymentMethodsScrollView: {
12811303
marginTop: 12,
1304+
},
1305+
paymentMethodsList: {
12821306
gap: 10,
12831307
},
1308+
paymentMethodHeaderRight: {
1309+
flexDirection: 'row',
1310+
alignItems: 'center',
1311+
gap: 8,
1312+
},
1313+
hideZeroLabel: {
1314+
fontSize: 12,
1315+
color: Colors.white,
1316+
opacity: 0.9,
1317+
fontWeight: '500',
1318+
textAlign: 'right',
1319+
},
12841320
paymentMethodItem: {
12851321
flexDirection: 'row',
12861322
justifyContent: 'space-between',

app/(tabs)/settings.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export default function SettingsScreen() {
101101
// External support links (leave empty for now — user will fill these later)
102102
const [faqUrl, setFaqUrl] = useState('https://github.com/BYLinMou/COMP3330-Gp20-AuraSpend/issues');
103103
const [contactUrl, setContactUrl] = useState('');
104-
const [tutorialUrl, setTutorialUrl] = useState('');
104+
const [tutorialUrl, setTutorialUrl] = useState('https://github.com/BYLinMou/COMP3330-Gp20-AuraSpend/blob/main/README.md');
105105
const [termsUrl, setTermsUrl] = useState('https://github.com/BYLinMou/COMP3330-Gp20-AuraSpend/blob/main/LICENSE');
106106
const [privacyUrl, setPrivacyUrl] = useState('');
107107

components/floating-chat-button.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
77
import { Colors } from '../constants/theme';
88
import { Gradients } from '../constants/theme';
99
import { sendChatCompletion, ChatMessage } from '../src/services/openai-client';
10-
import { allTools, SYSTEM_PROMPT, Tool, parseMultipleToolCalls, isValidToolName } from '../src/services/chat-tools';
10+
import { allTools, getSystemPromptWithTime, Tool, parseMultipleToolCalls, isValidToolName } from '../src/services/chat-tools';
1111
import { renderMarkdownAsReactNative } from '../src/utils/markdownHelper';
1212

1313
interface FloatingChatButtonProps {
@@ -168,7 +168,7 @@ export default function FloatingChatButton({ onPress }: FloatingChatButtonProps)
168168
// Prepare messages for AI
169169
// Filter out tool call messages and replace with execution summary
170170
const chatMessages: ChatMessage[] = [
171-
{ role: 'system', content: SYSTEM_PROMPT },
171+
{ role: 'system', content: getSystemPromptWithTime() },
172172
...updatedMessages.map(m => {
173173
if (m.isToolCall && m.toolCall) {
174174
// If the tool call has been executed with a result, include that context
@@ -376,7 +376,7 @@ export default function FloatingChatButton({ onPress }: FloatingChatButtonProps)
376376
// Prepare context for AI - show the tool result and let AI decide what to do next
377377
// AI can either: 1) Call more tools if needed, or 2) Provide a summary
378378
const toolResultContext: ChatMessage[] = [
379-
{ role: 'system', content: SYSTEM_PROMPT },
379+
{ role: 'system', content: getSystemPromptWithTime() },
380380
...allMessages,
381381
{
382382
role: 'user',

src/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"oneDayAgo": "1 day ago",
2222
"daysAgo": "{{count}} days ago",
2323
"paymentMethodBalances": "Payment Method Balances",
24+
"hideZeroBalances": "Hide zero balances",
2425
"noPaymentMethods": "No payment methods found",
2526
"category": "Category",
2627
"dateTime": "Date & Time",

src/locales/zh.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"oneDayAgo": "1 天前",
2222
"daysAgo": "{{count}} 天前",
2323
"paymentMethodBalances": "支付方式余额",
24+
"hideZeroBalances": "隐藏余额为 0 的项",
2425
"noPaymentMethods": "未找到支付方式",
2526
"category": "分类",
2627
"dateTime": "日期和时间",

src/services/chat-tools.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,8 @@ const toolsDescription = allTools.map(tool => {
661661
// System prompt for the AI assistant
662662
// Note: [FENCE] is used as placeholder for triple backticks to avoid template literal issues
663663
const FENCE = '```';
664+
import { getCurrentLocalTimeISO, getTimezoneOffset } from '../utils/datetime';
665+
664666
export const SYSTEM_PROMPT = `You are AuraSpend Assistant, a helpful AI assistant for the AuraSpend expense tracking app.
665667
666668
Your role is to help users manage their finances by providing information about their transactions, categories, budgets, and assisting with common tasks like adding expenses, categorizing transactions, and analyzing spending patterns.
@@ -840,3 +842,20 @@ ${FENCE}
840842
841843
Always be helpful, accurate, and EFFICIENT. Your goal is to complete tasks with the minimum number of API calls while providing excellent results.
842844
`;
845+
// Append a short note indicating the user's current local time to help the model resolve ambiguous dates/times
846+
// Returns a short note indicating the user's current local time to help the model resolve ambiguous dates/times
847+
export function getSystemPromptTimeNote(): string {
848+
const currentLocalTime = getCurrentLocalTimeISO();
849+
const tzOffset = getTimezoneOffset();
850+
return `\nUSER CURRENT LOCAL DATE/TIME\nThe user's current local time is: ${currentLocalTime} (timezone offset: ${tzOffset}).\nWhen dates/times are ambiguous or missing in user input or receipts, use this reference.\n`;
851+
}
852+
853+
// Returns a short note indicating the user's language preference
854+
export function getSystemPromptLanguageNote(userLanguage: string): string {
855+
return `\nUSER LANGUAGE PREFERENCE\nUser's selected language: ${userLanguage}\nRespond to the user in their preferred language (${userLanguage}).\n`;
856+
}
857+
858+
// Combined prompt with the time note and language note (dynamic)
859+
export function getSystemPromptWithTime(userLanguage?: string): string {
860+
return SYSTEM_PROMPT + getSystemPromptTimeNote() + (userLanguage ? getSystemPromptLanguageNote(userLanguage) : '');
861+
}

0 commit comments

Comments
 (0)