-feat 修复json格式问题
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
```
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user