-feat 修复json格式问题

This commit is contained in:
2026-01-28 21:48:50 +08:00
parent e163fadd01
commit 303b4ed001
2 changed files with 161 additions and 160 deletions

View File

@@ -1,4 +1,3 @@
```typescript
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { formatCurrency } from '../../../utils/format'; import { formatCurrency } from '../../../utils/format';
@@ -26,7 +25,7 @@ export const DailyInsightCard: React.FC<DailyInsightCardProps> = ({
lastWeekSpend, lastWeekSpend,
streakDays 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); const [isAiLoading, setIsAiLoading] = useState(false);
useEffect(() => { useEffect(() => {
@@ -34,31 +33,31 @@ export const DailyInsightCard: React.FC<DailyInsightCardProps> = ({
if (monthlyBudget === 0 && todaySpend === 0 && yesterdaySpend === 0) return; if (monthlyBudget === 0 && todaySpend === 0 && yesterdaySpend === 0) return;
const fetchAI = async () => { const fetchAI = async () => {
setIsAiLoading(true); setIsAiLoading(true);
try { try {
const today = new Date().getDate(); const today = new Date().getDate();
const daysInMonth = new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0).getDate(); const daysInMonth = new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0).getDate();
const result = await getDailyInsight({ const result = await getDailyInsight({
todaySpend, todaySpend,
yesterdaySpend, yesterdaySpend,
monthlyBudget, monthlyBudget,
monthlySpent, monthlySpent,
monthProgress: today / daysInMonth, monthProgress: today / daysInMonth,
topCategory, topCategory,
maxTransaction: maxTransaction ? { maxTransaction: maxTransaction ? {
note: maxTransaction.note || '', note: maxTransaction.note || '',
amount: maxTransaction.amount amount: maxTransaction.amount
} : undefined, } : undefined,
lastWeekSpend, lastWeekSpend,
streakDays streakDays
}); });
setAiData(result); setAiData(result);
} catch (e) { } catch (e) {
console.warn('AI insight fetch failed, sticking to local logic'); console.warn('AI insight fetch failed, sticking to local logic');
} finally { } finally {
setIsAiLoading(false); setIsAiLoading(false);
} }
}; };
const timer = setTimeout(fetchAI, 500); const timer = setTimeout(fetchAI, 500);
@@ -76,11 +75,11 @@ export const DailyInsightCard: React.FC<DailyInsightCardProps> = ({
if (Math.abs(diff) < 10) return { text: <span> <strong className="daily-insight__highlight">{formatCurrency(today)}</strong></span>, type: 'normal' }; if (Math.abs(diff) < 10) return { text: <span> <strong className="daily-insight__highlight">{formatCurrency(today)}</strong></span>, type: 'normal' };
if (today < yesterday) { if (today < yesterday) {
const percent = Math.round(((yesterday - today) / yesterday) * 100); const percent = Math.round(((yesterday - today) / yesterday) * 100);
return { text: <span>比昨天少花了 <strong className="daily-insight__highlight daily-insight__highlight--success">{percent}%</strong>!这就是进步,省下来的钱可以积少成多。</span>, type: 'success' }; return { text: <span> <strong className="daily-insight__highlight daily-insight__highlight--success">{percent}%</strong></span>, type: 'success' };
} else { } else {
const percent = Math.round(((today - yesterday) / yesterday) * 100); const percent = Math.round(((today - yesterday) / yesterday) * 100);
return { text: <span>比昨天多花了 <strong className="daily-insight__highlight daily-insight__highlight--warning">{percent}%</strong>。如果不是必需品,记得明天稍微控制一下哦。</span>, type: 'warning' }; return { text: <span> <strong className="daily-insight__highlight daily-insight__highlight--warning">{percent}%</strong></span>, type: 'warning' };
} }
}; };
@@ -97,48 +96,47 @@ export const DailyInsightCard: React.FC<DailyInsightCardProps> = ({
if (ratio >= 1) return { text: <span className="daily-insight__highlight daily-insight__highlight--danger"></span>, type: 'danger' }; if (ratio >= 1) return { text: <span className="daily-insight__highlight daily-insight__highlight--danger"></span>, type: 'danger' };
if (ratio > timeRatio + 0.15) { if (ratio > timeRatio + 0.15) {
return { text: <span>进度 <strong className="daily-insight__highlight daily-insight__highlight--warning">{Math.round(ratio*100)}%</strong> (时间 {Math.round(timeRatio*100)}%)。花钱速度有点快了,建议踩踩刹车。</span>, type: 'warning' }; return { text: <span> <strong className="daily-insight__highlight daily-insight__highlight--warning">{Math.round(ratio * 100)}%</strong> ( {Math.round(timeRatio * 100)}%)</span>, type: 'warning' };
} }
if (ratio < timeRatio - 0.1) { if (ratio < timeRatio - 0.1) {
return { text: <span>进度 <strong className="daily-insight__highlight daily-insight__highlight--success">{Math.round(ratio*100)}%</strong> (时间 {Math.round(timeRatio*100)}%)。控制得非常完美,月底可能有惊喜结余!</span>, type: 'success' }; return { text: <span> <strong className="daily-insight__highlight daily-insight__highlight--success">{Math.round(ratio * 100)}%</strong> ( {Math.round(timeRatio * 100)}%)</span>, type: 'success' };
} }
return { text: <span>进度 <strong>{Math.round(ratio*100)}%</strong> (时间 {Math.round(timeRatio*100)}%)。目前节奏刚刚好,稳扎稳打。</span>, type: 'normal' }; return { text: <span> <strong>{Math.round(ratio * 100)}%</strong> ( {Math.round(timeRatio * 100)}%)</span>, type: 'normal' };
}; };
const spendingInsight = getSpendingInsight(todaySpend, yesterdaySpend); const spendingInsight = getSpendingInsight(todaySpend, yesterdaySpend);
const budgetInsight = getBudgetInsight(monthlySpent, monthlyBudget); const budgetInsight = getBudgetInsight(monthlySpent, monthlyBudget);
return ( return (
<div className={`daily - insight - card ${ aiData ? 'daily-insight-card--ai' : '' } `}> <div className={`daily-insight-card ${aiData ? 'daily-insight-card--ai' : ''}`}>
<div className="daily-insight__header"> <div className="daily-insight__header">
<Icon icon={aiData ? "solar:magic-stick-3-bold-duotone" : "solar:stars-minimalistic-bold-duotone"} width="20" /> <Icon icon={aiData ? "solar:magic-stick-3-bold-duotone" : "solar:stars-minimalistic-bold-duotone"} width="20" />
<span>{aiData ? 'AI 每日简报' : '每日简报'}</span> <span>{aiData ? 'AI 每日简报' : '每日简报'}</span>
{isAiLoading && !aiData && <span className="daily-insight__loading-badge">AI 思考中...</span>} {isAiLoading && !aiData && <span className="daily-insight__loading-badge">AI ...</span>}
</div> </div>
<div className="daily-insight__content"> <div className="daily-insight__content">
<div className="daily-insight__section"> <div className="daily-insight__section">
<div className="section-header-row"> <div className="section-header-row">
<span className="daily-insight__title">今日消费</span> <span className="daily-insight__title"></span>
{lastWeekSpend !== undefined && lastWeekSpend > 0 && ( {lastWeekSpend !== undefined && lastWeekSpend > 0 && (
<span className={`week - diff - badge ${ todaySpend <= lastWeekSpend ? 'green' : 'red' } `}> <span className={`week-diff-badge ${todaySpend <= lastWeekSpend ? 'green' : 'red'}`}>
周同比 {todaySpend <= lastWeekSpend ? '↓' : '↑'}{Math.abs(todaySpend - lastWeekSpend).toFixed(0)} {todaySpend <= lastWeekSpend ? '↓' : '↑'}{Math.abs(todaySpend - lastWeekSpend).toFixed(0)}
</span> </span>
)} )}
</div>
<p className="daily-insight__text animate-fade-in">{spendingInsight.text}</p>
</div> </div>
<p className="daily-insight__text animate-fade-in">{spendingInsight.text}</p>
</div>
<div className="daily-insight__divider" /> <div className="daily-insight__divider" />
<div className="daily-insight__section"> <div className="daily-insight__section">
<span className="daily-insight__title">预算风向标</span> <span className="daily-insight__title"></span>
<p className="daily-insight__text animate-fade-in">{budgetInsight.text}</p> <p className="daily-insight__text animate-fade-in">{budgetInsight.text}</p>
</div> </div>
</div> </div>
</div> </div>
); );
}; };
```

View File

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