feat: 添加智能记账AI服务,包括聊天、语音输入、交易确认和财务建议,并集成到首页展示。
This commit is contained in:
@@ -21,6 +21,7 @@ interface DailyInsightCardProps {
|
|||||||
top3Categories?: { name: string; amount: number }[];
|
top3Categories?: { name: string; amount: number }[];
|
||||||
todayTransactionCount?: number;
|
todayTransactionCount?: number;
|
||||||
last7DaysSpend?: number[]; // 最近7天每日支出
|
last7DaysSpend?: number[]; // 最近7天每日支出
|
||||||
|
recentTransactions?: { date: string; categoryName: string; amount: number; type: string; note?: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DailyInsightCard: React.FC<DailyInsightCardProps> = ({
|
export const DailyInsightCard: React.FC<DailyInsightCardProps> = ({
|
||||||
@@ -39,6 +40,7 @@ export const DailyInsightCard: React.FC<DailyInsightCardProps> = ({
|
|||||||
top3Categories,
|
top3Categories,
|
||||||
todayTransactionCount,
|
todayTransactionCount,
|
||||||
last7DaysSpend,
|
last7DaysSpend,
|
||||||
|
recentTransactions,
|
||||||
}) => {
|
}) => {
|
||||||
const [aiData, setAiData] = useState<{ spending: string; budget: string; emoji?: string; tip?: string } | null>(null);
|
const [aiData, setAiData] = useState<{ spending: string; budget: string; emoji?: string; tip?: string } | null>(null);
|
||||||
const [isAiLoading, setIsAiLoading] = useState(false);
|
const [isAiLoading, setIsAiLoading] = useState(false);
|
||||||
@@ -74,6 +76,7 @@ export const DailyInsightCard: React.FC<DailyInsightCardProps> = ({
|
|||||||
top3Categories,
|
top3Categories,
|
||||||
todayTransactionCount,
|
todayTransactionCount,
|
||||||
last7DaysSpend,
|
last7DaysSpend,
|
||||||
|
recentTransactions,
|
||||||
});
|
});
|
||||||
setAiData(result);
|
setAiData(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -85,7 +88,7 @@ export const DailyInsightCard: React.FC<DailyInsightCardProps> = ({
|
|||||||
|
|
||||||
const timer = setTimeout(fetchAI, 500);
|
const timer = setTimeout(fetchAI, 500);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [todaySpend, yesterdaySpend, monthlyBudget, monthlySpent, topCategory, maxTransaction, lastWeekSpend, streakDays, budgetRemaining, daysRemaining, weeklyTotal, avgDailySpend, top3Categories, todayTransactionCount, last7DaysSpend]);
|
}, [todaySpend, yesterdaySpend, monthlyBudget, monthlySpent, topCategory, maxTransaction, lastWeekSpend, streakDays, budgetRemaining, daysRemaining, weeklyTotal, avgDailySpend, top3Categories, todayTransactionCount, last7DaysSpend, recentTransactions]);
|
||||||
|
|
||||||
const getSpendingInsight = (today: number, yesterday: number) => {
|
const getSpendingInsight = (today: number, yesterday: number) => {
|
||||||
if (aiData) return { text: <span>{aiData.spending}</span>, type: 'ai' };
|
if (aiData) return { text: <span>{aiData.spending}</span>, type: 'ai' };
|
||||||
|
|||||||
@@ -30,20 +30,21 @@
|
|||||||
.home-greeting {
|
.home-greeting {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
gap: 0.75rem;
|
||||||
/* Tighter gap */
|
/* Increased gap for better breathing room */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ... */
|
/* ... */
|
||||||
|
|
||||||
.greeting-text {
|
.greeting-text {
|
||||||
font-family: 'Outfit', sans-serif;
|
font-family: 'Outfit', sans-serif;
|
||||||
font-size: 1.5rem;
|
font-size: 1.75rem;
|
||||||
/* Slightly smaller for compactness */
|
/* Restored size */
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin: 0;
|
margin: 0.25rem 0;
|
||||||
|
/* Added vertical margin */
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
line-height: 1.2;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ... */
|
/* ... */
|
||||||
|
|||||||
@@ -207,6 +207,16 @@ function Home() {
|
|||||||
};
|
};
|
||||||
}, [todayTransactions, weekTransactions, categories, monthlyBudgetSpentTotal]);
|
}, [todayTransactions, weekTransactions, categories, monthlyBudgetSpentTotal]);
|
||||||
|
|
||||||
|
const recentTransactionsForAI = useMemo(() => {
|
||||||
|
return weekTransactions.map(t => ({
|
||||||
|
date: new Date(t.transactionDate).toLocaleDateString('zh-CN'),
|
||||||
|
categoryName: categories.find(c => c.id === t.categoryId)?.name || '未知',
|
||||||
|
amount: t.amount,
|
||||||
|
type: t.type,
|
||||||
|
note: t.note
|
||||||
|
}));
|
||||||
|
}, [weekTransactions, categories]);
|
||||||
|
|
||||||
const handleQuickTransaction = () => {
|
const handleQuickTransaction = () => {
|
||||||
navigate('/transactions?action=new');
|
navigate('/transactions?action=new');
|
||||||
};
|
};
|
||||||
@@ -513,6 +523,7 @@ function Home() {
|
|||||||
lastWeekSpend={lastWeekSpend}
|
lastWeekSpend={lastWeekSpend}
|
||||||
streakDays={streakInfo?.currentStreak || 0}
|
streakDays={streakInfo?.currentStreak || 0}
|
||||||
budgetRemaining={monthlyBudgetTotal - monthlyBudgetSpentTotal}
|
budgetRemaining={monthlyBudgetTotal - monthlyBudgetSpentTotal}
|
||||||
|
recentTransactions={recentTransactionsForAI}
|
||||||
avgDailySpend={avgDailySpend}
|
avgDailySpend={avgDailySpend}
|
||||||
top3Categories={top3Categories}
|
top3Categories={top3Categories}
|
||||||
todayTransactionCount={todayTransactionCount}
|
todayTransactionCount={todayTransactionCount}
|
||||||
|
|||||||
@@ -319,6 +319,7 @@ export interface DailyInsightContext {
|
|||||||
// 分类分析
|
// 分类分析
|
||||||
topCategory?: { name: string; amount: number };
|
topCategory?: { name: string; amount: number };
|
||||||
top3Categories?: { name: string; amount: number }[]; // 本月前三分类
|
top3Categories?: { name: string; amount: number }[]; // 本月前三分类
|
||||||
|
recentTransactions?: { date: string; categoryName: string; amount: number; type: string; note?: string }[]; // 最近7天交易详情
|
||||||
|
|
||||||
// 交易详情
|
// 交易详情
|
||||||
maxTransaction?: { note: string; amount: number };
|
maxTransaction?: { note: string; amount: number };
|
||||||
@@ -333,7 +334,9 @@ export interface DailyInsightContext {
|
|||||||
*/
|
*/
|
||||||
export async function getDailyInsight(context: DailyInsightContext): Promise<{ spending: string; budget: string; emoji?: string; tip?: string }> {
|
export async function getDailyInsight(context: DailyInsightContext): Promise<{ spending: string; budget: string; emoji?: string; tip?: string }> {
|
||||||
// Hash needs to include new fields
|
// Hash needs to include new fields
|
||||||
const currentHash = JSON.stringify(context);
|
// Simplify recentTransactions for hash to avoid order issues or too long string
|
||||||
|
const contextForHash = { ...context, recentTransactions: context.recentTransactions?.length };
|
||||||
|
const currentHash = JSON.stringify(contextForHash);
|
||||||
const NOW = Date.now();
|
const NOW = Date.now();
|
||||||
const CACHE_TTL = 30 * 60 * 1000; // 30 Minutes
|
const CACHE_TTL = 30 * 60 * 1000; // 30 Minutes
|
||||||
|
|
||||||
@@ -348,6 +351,12 @@ export async function getDailyInsight(context: DailyInsightContext): Promise<{ s
|
|||||||
const weekday = new Date().toLocaleDateString('zh-CN', { weekday: 'long' });
|
const weekday = new Date().toLocaleDateString('zh-CN', { weekday: 'long' });
|
||||||
const weekDiff = context.lastWeekSpend !== undefined ? (context.todaySpend - context.lastWeekSpend) : 0;
|
const weekDiff = context.lastWeekSpend !== undefined ? (context.todaySpend - context.lastWeekSpend) : 0;
|
||||||
|
|
||||||
|
// Format recent transactions for AI (simplify to save tokens)
|
||||||
|
const formattedTransactions = context.recentTransactions
|
||||||
|
?.slice(0, 50) // Limit to 50 items
|
||||||
|
.map(t => `${t.date}: ${t.categoryName} ${t.type === 'expense' ? '-' : '+'}${t.amount.toFixed(1)}${t.note ? ` (${t.note})` : ''}`)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
// Prepare enriched context for the backend
|
// Prepare enriched context for the backend
|
||||||
// We format some fields to help the AI understand better (like percentages)
|
// We format some fields to help the AI understand better (like percentages)
|
||||||
const enrichedContext = {
|
const enrichedContext = {
|
||||||
@@ -356,7 +365,8 @@ export async function getDailyInsight(context: DailyInsightContext): Promise<{ s
|
|||||||
weekDiff,
|
weekDiff,
|
||||||
date: new Date().toLocaleDateString('zh-CN'),
|
date: new Date().toLocaleDateString('zh-CN'),
|
||||||
monthProgressPercent: `${(context.monthProgress * 100).toFixed(0)}%`,
|
monthProgressPercent: `${(context.monthProgress * 100).toFixed(0)}%`,
|
||||||
budgetUsedPercent: `${(context.monthlySpent / (context.monthlyBudget || 1) * 100).toFixed(0)}%`
|
budgetUsedPercent: `${(context.monthlySpent / (context.monthlyBudget || 1) * 100).toFixed(0)}%`,
|
||||||
|
recentTransactionsSummary: formattedTransactions // Send as specific field
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user