From 303b4ed00105815bd3fb6e3a84e906ddaa0b4144 Mon Sep 17 00:00:00 2001 From: 12975 <1297598740@qq.com> Date: Wed, 28 Jan 2026 21:48:50 +0800 Subject: [PATCH] =?UTF-8?q?-feat=20=E4=BF=AE=E5=A4=8Djson=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DailyInsightCard/DailyInsightCard.tsx | 136 +++++++------ src/services/aiService.ts | 185 +++++++++--------- 2 files changed, 161 insertions(+), 160 deletions(-) diff --git a/src/components/home/DailyInsightCard/DailyInsightCard.tsx b/src/components/home/DailyInsightCard/DailyInsightCard.tsx index e7fb413..ec9400e 100644 --- a/src/components/home/DailyInsightCard/DailyInsightCard.tsx +++ b/src/components/home/DailyInsightCard/DailyInsightCard.tsx @@ -1,4 +1,3 @@ -```typescript import React, { useState, useEffect } from 'react'; import { Icon } from '@iconify/react'; import { formatCurrency } from '../../../utils/format'; @@ -26,7 +25,7 @@ export const DailyInsightCard: React.FC = ({ lastWeekSpend, streakDays }) => { - const [aiData, setAiData] = useState<{spending: string; budget: string} | null>(null); + const [aiData, setAiData] = useState<{ spending: string; budget: string } | null>(null); const [isAiLoading, setIsAiLoading] = useState(false); useEffect(() => { @@ -34,31 +33,31 @@ export const DailyInsightCard: React.FC = ({ if (monthlyBudget === 0 && todaySpend === 0 && yesterdaySpend === 0) return; const fetchAI = async () => { - setIsAiLoading(true); - try { - const today = new Date().getDate(); - const daysInMonth = new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0).getDate(); - - const result = await getDailyInsight({ - todaySpend, - yesterdaySpend, - monthlyBudget, - monthlySpent, - monthProgress: today / daysInMonth, - topCategory, - maxTransaction: maxTransaction ? { - note: maxTransaction.note || '', - amount: maxTransaction.amount - } : undefined, - lastWeekSpend, - streakDays - }); - setAiData(result); - } catch (e) { - console.warn('AI insight fetch failed, sticking to local logic'); - } finally { - setIsAiLoading(false); - } + setIsAiLoading(true); + try { + const today = new Date().getDate(); + const daysInMonth = new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0).getDate(); + + const result = await getDailyInsight({ + todaySpend, + yesterdaySpend, + monthlyBudget, + monthlySpent, + monthProgress: today / daysInMonth, + topCategory, + maxTransaction: maxTransaction ? { + note: maxTransaction.note || '', + amount: maxTransaction.amount + } : undefined, + lastWeekSpend, + streakDays + }); + setAiData(result); + } catch (e) { + console.warn('AI insight fetch failed, sticking to local logic'); + } finally { + setIsAiLoading(false); + } }; const timer = setTimeout(fetchAI, 500); @@ -69,18 +68,18 @@ export const DailyInsightCard: React.FC = ({ if (aiData) return { text: {aiData.spending}, type: 'ai' }; if (today === 0) return { text: "今天还没有花钱,保持这种“零消费”状态就是最大的赚钱!", type: 'success' }; - + if (yesterday === 0) return { text: 今天花了 {formatCurrency(today)},既然昨天没花钱,今天稍微奢侈一点也无妨。, type: 'normal' }; const diff = today - yesterday; if (Math.abs(diff) < 10) return { text: 今天支出 {formatCurrency(today)},跟昨天差不多,生活节奏很稳。, type: 'normal' }; - + if (today < yesterday) { - const percent = Math.round(((yesterday - today) / yesterday) * 100); - return { text: 比昨天少花了 {percent}%!这就是进步,省下来的钱可以积少成多。, type: 'success' }; + const percent = Math.round(((yesterday - today) / yesterday) * 100); + return { text: 比昨天少花了 {percent}%!这就是进步,省下来的钱可以积少成多。, type: 'success' }; } else { - const percent = Math.round(((today - yesterday) / yesterday) * 100); - return { text: 比昨天多花了 {percent}%。如果不是必需品,记得明天稍微控制一下哦。, type: 'warning' }; + const percent = Math.round(((today - yesterday) / yesterday) * 100); + return { text: 比昨天多花了 {percent}%。如果不是必需品,记得明天稍微控制一下哦。, type: 'warning' }; } }; @@ -88,57 +87,56 @@ export const DailyInsightCard: React.FC = ({ if (aiData) return { text: {aiData.budget}, type: 'ai' }; if (total === 0) return { text: "您还没有设置月度预算,建议去设置一个。", type: 'normal' }; - + const ratio = spent / total; const today = new Date().getDate(); const daysInMonth = new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0).getDate(); - const timeRatio = today / daysInMonth; - + const timeRatio = today / daysInMonth; + if (ratio >= 1) return { text: 本月预算已耗尽!接下来的每一天都需要极限生存挑战了。, type: 'danger' }; - + if (ratio > timeRatio + 0.15) { - return { text: 进度 {Math.round(ratio*100)}% (时间 {Math.round(timeRatio*100)}%)。花钱速度有点快了,建议踩踩刹车。, type: 'warning' }; + return { text: 进度 {Math.round(ratio * 100)}% (时间 {Math.round(timeRatio * 100)}%)。花钱速度有点快了,建议踩踩刹车。, type: 'warning' }; } - + if (ratio < timeRatio - 0.1) { - return { text: 进度 {Math.round(ratio*100)}% (时间 {Math.round(timeRatio*100)}%)。控制得非常完美,月底可能有惊喜结余!, type: 'success' }; + return { text: 进度 {Math.round(ratio * 100)}% (时间 {Math.round(timeRatio * 100)}%)。控制得非常完美,月底可能有惊喜结余!, type: 'success' }; } - - return { text: 进度 {Math.round(ratio*100)}% (时间 {Math.round(timeRatio*100)}%)。目前节奏刚刚好,稳扎稳打。, type: 'normal' }; + + return { text: 进度 {Math.round(ratio * 100)}% (时间 {Math.round(timeRatio * 100)}%)。目前节奏刚刚好,稳扎稳打。, type: 'normal' }; }; const spendingInsight = getSpendingInsight(todaySpend, yesterdaySpend); const budgetInsight = getBudgetInsight(monthlySpent, monthlyBudget); return ( -
-
- - {aiData ? 'AI 每日简报' : '每日简报'} - {isAiLoading && !aiData && AI 思考中...} -
- -
-
-
- 今日消费 - {lastWeekSpend !== undefined && lastWeekSpend > 0 && ( - - 周同比 {todaySpend <= lastWeekSpend ? '↓' : '↑'}{Math.abs(todaySpend - lastWeekSpend).toFixed(0)} - - )} -
-

{spendingInsight.text}

+
+
+ + {aiData ? 'AI 每日简报' : '每日简报'} + {isAiLoading && !aiData && AI 思考中...} +
+ +
+
+
+ 今日消费 + {lastWeekSpend !== undefined && lastWeekSpend > 0 && ( + + 周同比 {todaySpend <= lastWeekSpend ? '↓' : '↑'}{Math.abs(todaySpend - lastWeekSpend).toFixed(0)} + + )}
- -
- -
- 预算风向标 -

{budgetInsight.text}

-
-
+

{spendingInsight.text}

+
+ +
+ +
+ 预算风向标 +

{budgetInsight.text}

+
+
); }; -``` diff --git a/src/services/aiService.ts b/src/services/aiService.ts index f7bb85b..affd832 100644 --- a/src/services/aiService.ts +++ b/src/services/aiService.ts @@ -289,45 +289,47 @@ Output Requirements: // Here we choose to throw so fallback static text appears. throw error; } - // Cache storage for daily insight - let dailyInsightCache: { - data: { spending: string; budget: string }; - timestamp: number; - contentHash: string; - } | null = null; +} - export interface DailyInsightContext { - todaySpend: number; - yesterdaySpend: number; - monthlyBudget: number; - monthlySpent: number; - monthProgress: number; // 0-1 - topCategory?: { name: string; amount: number }; - maxTransaction?: { note: string; amount: number }; - lastWeekSpend?: number; - streakDays?: number; +// Cache storage for daily insight +let dailyInsightCache: { + data: { spending: string; budget: string }; + timestamp: number; + contentHash: string; +} | null = null; + +export interface DailyInsightContext { + todaySpend: number; + yesterdaySpend: number; + monthlyBudget: number; + monthlySpent: number; + monthProgress: number; // 0-1 + topCategory?: { name: string; amount: number }; + maxTransaction?: { note: string; amount: number }; + lastWeekSpend?: number; + streakDays?: number; +} + +/** + * Get AI-powered daily insight + */ +export async function getDailyInsight(context: DailyInsightContext): Promise<{ spending: string; budget: string }> { + // Hash needs to include new fields + const currentHash = JSON.stringify(context); + const NOW = Date.now(); + const CACHE_TTL = 30 * 60 * 1000; // 30 Minutes + + // 1. Check Cache + if (dailyInsightCache && + (NOW - dailyInsightCache.timestamp < CACHE_TTL) && + dailyInsightCache.contentHash === currentHash) { + return dailyInsightCache.data; } - /** - * Get AI-powered daily insight - */ - export async function getDailyInsight(context: DailyInsightContext): Promise<{ spending: string; budget: string }> { - // Hash needs to include new fields - const currentHash = JSON.stringify(context); - const NOW = Date.now(); - const CACHE_TTL = 30 * 60 * 1000; // 30 Minutes + const weekday = new Date().toLocaleDateString('zh-CN', { weekday: 'long' }); + const weekDiff = context.lastWeekSpend !== undefined ? (context.todaySpend - context.lastWeekSpend) : 0; - // 1. Check Cache - if (dailyInsightCache && - (NOW - dailyInsightCache.timestamp < CACHE_TTL) && - dailyInsightCache.contentHash === currentHash) { - return dailyInsightCache.data; - } - - const weekday = new Date().toLocaleDateString('zh-CN', { weekday: 'long' }); - const weekDiff = context.lastWeekSpend !== undefined ? (context.todaySpend - context.lastWeekSpend) : 0; - - const prompt = `System: 你是 Novault 首席财务AI,也是用户的贴身管家。你的点评需要非常有温度、有依据。 + const prompt = `System: 你是 Novault 首席财务AI,也是用户的贴身管家。你的点评需要非常有温度、有依据。 Context: - 今天是: ${weekday} - 连续记账: ${context.streakDays || 0} 天 @@ -351,63 +353,64 @@ Task: - 结合月度进度,给出具体行动指南。 `; - try { - const response = await sendChatMessage(prompt); + try { + const response = await sendChatMessage(prompt); - if (response.message) { - let content = response.message.trim(); + if (response.message) { + let content = response.message.trim(); - // Extract JSON object using regex to handle potential markdown or extra text - const jsonMatch = content.match(/\{[\s\S]*\}/); - if (jsonMatch) { - content = jsonMatch[0]; - } else { - // Fallback cleanup if regex fails but it might still be JSON-ish - content = content.replace(/^```json\s*/, '').replace(/\s*```$/, ''); - } - - let parsed; - try { - parsed = JSON.parse(content); - } catch (e) { - console.warn('AI returned invalid JSON, falling back to raw text split or default', content); - // Fallback: simple split if possible or default - parsed = { - spending: content.slice(0, 50) + '...', - budget: 'AI 分析数据格式异常,请稍后再试。' - }; - } - - const result = { - spending: parsed.spending || '暂无点评', - budget: parsed.budget || '暂无建议' - }; - - // Update Cache - dailyInsightCache = { - data: result, - timestamp: NOW, - contentHash: currentHash - }; - - return result; + // Extract JSON object using regex to handle potential markdown or extra text + const jsonMatch = content.match(/\{[\s\S]*\}/); + if (jsonMatch) { + content = jsonMatch[0]; + } else { + // Fallback cleanup if regex fails but it might still be JSON-ish + content = content.replace(/^```json\s*/, '').replace(/\s*```$/, ''); } - throw new Error('No insight received'); - } catch (error) { - console.error('Failed to get AI insight:', error); - throw error; - } - } - export default { - getSessionId, - clearSession, - sendChatMessage, - transcribeAudio, - confirmTransaction, - cancelSession, - processVoiceInput, - isConfirmationCardComplete, - formatConfirmationCard, - getFinancialAdvice, - }; + let parsed; + try { + parsed = JSON.parse(content); + } catch (e) { + console.warn('AI returned invalid JSON, falling back to raw text split or default', content); + // Fallback: simple split if possible or default + parsed = { + spending: content.slice(0, 50) + '...', + budget: 'AI 分析数据格式异常,请稍后再试。' + }; + } + + const result = { + spending: parsed.spending || '暂无点评', + budget: parsed.budget || '暂无建议' + }; + + // Update Cache + dailyInsightCache = { + data: result, + timestamp: NOW, + contentHash: currentHash + }; + + return result; + } + throw new Error('No insight received'); + } catch (error) { + console.error('Failed to get AI insight:', error); + throw error; + } +} + +export default { + getSessionId, + clearSession, + sendChatMessage, + transcribeAudio, + confirmTransaction, + cancelSession, + processVoiceInput, + isConfirmationCardComplete, + formatConfirmationCard, + getFinancialAdvice, + getDailyInsight, +};