import React, { useState, useEffect, useRef } from 'react'; import { processUserMessage } from './services/geminiService'; import VoiceInput from './components/VoiceInput'; import LedgerPage from './components/LedgerPage'; import StatisticsPage from './components/StatisticsPage'; import EditTransactionModal from './components/EditTransactionModal'; import { Transaction, TransactionType, Message, ExpenseCategory, IncomeCategory, BillReminder } from './types'; import { v4 as uuidv4 } from 'uuid'; type Tab = 'chat' | 'ledger' | 'stats'; interface AISettings { name: string; persona: string; avatar?: string; } const DEFAULT_AI_SETTINGS: AISettings = { name: "崔经理", persona: "一个精明、毒舌但实际上很负责任的财务经理。说话喜欢带刺,吐槽用户乱花钱,但关键时刻会给出靠谱建议。" }; const INITIAL_PERSONA_PRESETS = [ { id: 'toxic', name: '崔经理 (默认)', description: '精明、毒舌、傲娇。喜欢吐槽你乱花钱,"恨铁不成钢",但内心其实是为你好。' }, { id: 'gentle', name: '温柔管家', description: '温柔、体贴、治愈。无论你怎么花钱都会温柔记录,并在超支时委婉地提醒你注意身体和钱包。' }, { id: 'strict', name: '严厉会计', description: '一丝不苟、冷酷无情。只关心数字,对任何非必要支出都会严厉批评,没有任何废话。' }, { id: 'custom', name: '自定义', description: '可以根据你喜欢的方式设定助手的语气和风格' } ]; const getLocalDateString = (date = new Date()) => { const offset = date.getTimezoneOffset(); const localDate = new Date(date.getTime() - (offset * 60 * 1000)); return localDate.toISOString().split('T')[0]; }; const getCategoryIcon = (category: string, type: TransactionType) => { if (type === TransactionType.INCOME) { switch (category) { case IncomeCategory.SALARY: return '💰'; case IncomeCategory.BONUS: return '🧧'; case IncomeCategory.INVESTMENT: return '📈'; default: return '💵'; } } switch (category) { case ExpenseCategory.FOOD: return '🍔'; case ExpenseCategory.SHOPPING: return '🛍️'; case ExpenseCategory.TRANSPORT: return '🚗'; case ExpenseCategory.HOUSING: return '🏠'; case ExpenseCategory.ENTERTAINMENT: return '🎬'; case ExpenseCategory.MEDICAL: return '💊'; case ExpenseCategory.SOCIAL: return '🤝'; case ExpenseCategory.EDUCATION: return '📚'; case ExpenseCategory.FINANCE: return '🏦'; case ExpenseCategory.CHILD: return '👶'; case ExpenseCategory.PET: return '🐱'; case ExpenseCategory.CAR: return '🚘'; case ExpenseCategory.BEAUTY: return '💄'; case ExpenseCategory.BUSINESS: return '💼'; case ExpenseCategory.MISC: return '🧺'; default: return '🧾'; } }; const App: React.FC = () => { const [activeTab, setActiveTab] = useState('chat'); const [messages, setMessages] = useState([]); const [transactions, setTransactions] = useState([]); const [budgets, setBudgets] = useState>({ '总预算': 3000 }); const [reminders, setReminders] = useState([]); const [inputText, setInputText] = useState(''); const [isProcessing, setIsProcessing] = useState(false); const [editingTransaction, setEditingTransaction] = useState(null); const [isEditingFromChat, setIsEditingFromChat] = useState(false); const [inputMode, setInputMode] = useState<'voice' | 'keyboard'>('voice'); const [aiSettings, setAiSettings] = useState(DEFAULT_AI_SETTINGS); const [isSettingsOpen, setIsSettingsOpen] = useState(false); const [customPreset, setCustomPreset] = useState({ name: '自定义', description: '可以根据你喜欢的方式设定助手的语气和风格' }); const fileInputRef = useRef(null); const avatarInputRef = useRef(null); const messagesEndRef = useRef(null); useEffect(() => { const storedTransactions = localStorage.getItem('cui_transactions'); const storedMessages = localStorage.getItem('cui_messages'); const storedBudget = localStorage.getItem('cui_budget'); const storedReminders = localStorage.getItem('cui_reminders'); const storedAiSettings = localStorage.getItem('cui_ai_settings'); const storedCustom = localStorage.getItem('cui_custom_preset_info'); if (storedTransactions) setTransactions(JSON.parse(storedTransactions)); if (storedMessages) setMessages(JSON.parse(storedMessages)); if (storedBudget) { const parsed = JSON.parse(storedBudget); setBudgets(typeof parsed === 'number' ? { '总预算': parsed } : parsed); } if (storedReminders) setReminders(JSON.parse(storedReminders)); if (storedAiSettings) setAiSettings(JSON.parse(storedAiSettings)); if (storedCustom) setCustomPreset(JSON.parse(storedCustom)); if (!storedMessages) { setMessages([{ id: 'init', role: 'assistant', content: `我是${storedAiSettings ? JSON.parse(storedAiSettings).name : '崔经理'}。✨\n想记账?快说!🎤`, timestamp: Date.now() }]); } }, []); useEffect(() => { localStorage.setItem('cui_transactions', JSON.stringify(transactions)); }, [transactions]); useEffect(() => { localStorage.setItem('cui_budget', JSON.stringify(budgets)); }, [budgets]); useEffect(() => { localStorage.setItem('cui_reminders', JSON.stringify(reminders)); }, [reminders]); useEffect(() => { localStorage.setItem('cui_ai_settings', JSON.stringify(aiSettings)); }, [aiSettings]); useEffect(() => { localStorage.setItem('cui_custom_preset_info', JSON.stringify(customPreset)); }, [customPreset]); useEffect(() => { localStorage.setItem('cui_messages', JSON.stringify(messages)); if (activeTab === 'chat') scrollToBottom(); }, [messages, activeTab]); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }; const handleUpdateTransaction = (updated: Transaction) => { setTransactions(prev => prev.map(t => t.id === updated.id ? updated : t)); setEditingTransaction(null); setIsEditingFromChat(false); }; const handleDeleteTransaction = (id: string) => { if (confirm('确定要删除这条记录吗?')) { setTransactions(prev => prev.filter(t => t.id !== id)); setEditingTransaction(null); setIsEditingFromChat(false); } }; const handleAddReminder = (reminder: BillReminder) => { setReminders(prev => [...prev, reminder]); }; const handleUpdateReminder = (updated: BillReminder) => { setReminders(prev => prev.map(r => r.id === updated.id ? updated : r)); }; const handleDeleteReminder = (id: string) => { if (confirm('确定要删除这个提醒吗?')) { setReminders(prev => prev.filter(r => r.id !== id)); } }; const handleToggleReminder = (id: string) => { setReminders(prev => prev.map(r => r.id === id ? { ...r, isCompleted: !r.isCompleted } : r)); }; const handleSendMessage = async (text: string, imageBase64?: string, mimeType?: string) => { if (!text.trim() && !imageBase64) return; const userMsg: Message = { id: uuidv4(), role: 'user', content: imageBase64 ? '已上传图片' : text, timestamp: Date.now() }; setMessages(prev => [...prev, userMsg]); setInputText(''); setIsProcessing(true); try { const result = await processUserMessage(text, aiSettings.name, aiSettings.persona, imageBase64, mimeType); if (result.transaction) { const newTransactionId = uuidv4(); const newTransaction: Transaction = { id: newTransactionId, item: result.transaction.item, amount: result.transaction.amount, type: result.transaction.type, category: result.transaction.category, date: getLocalDateString(), timestamp: Date.now() }; setTransactions(prev => [...prev, newTransaction]); setMessages(prev => [...prev, { id: uuidv4(), role: 'assistant', content: 'RECEIPT', timestamp: Date.now(), relatedTransactionId: newTransactionId }]); const replySegments = result.reply.split('\n').filter(s => s.trim()).slice(0, 2); for (let i = 0; i < replySegments.length; i++) { if (i > 0) await new Promise(r => setTimeout(r, 450)); setMessages(prev => [...prev, { id: uuidv4(), role: 'assistant', content: replySegments[i].trim(), timestamp: Date.now() }]); } } else { const replySegments = result.reply.split('\n').filter(s => s.trim()).slice(0, 2); for (let i = 0; i < replySegments.length; i++) { if (i > 0) await new Promise(r => setTimeout(r, 450)); setMessages(prev => [...prev, { id: uuidv4(), role: 'assistant', content: replySegments[i].trim(), timestamp: Date.now() }]); } } } catch (error) { setMessages(prev => [...prev, { id: uuidv4(), role: 'assistant', content: '系统走神了... 🫥\n等会儿再说。', timestamp: Date.now() }]); } finally { setIsProcessing(false); } }; const handleKeyPress = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSendMessage(inputText); } }; const handleFileUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onloadend = () => { const base64Data = (reader.result as string).split(',')[1]; handleSendMessage("", base64Data, file.type); }; reader.readAsDataURL(file); if (fileInputRef.current) fileInputRef.current.value = ''; } const handleAvatarUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onloadend = () => { const base64Data = reader.result as string; setAiSettings(prev => ({ ...prev, avatar: base64Data })); }; reader.readAsDataURL(file); if (avatarInputRef.current) avatarInputRef.current.value = ''; } const todayStr = getLocalDateString(); const todaySpending = transactions .filter(t => t.date === todayStr && t.type === TransactionType.EXPENSE) .reduce((a, b) => a + b.amount, 0); const personaPresets = INITIAL_PERSONA_PRESETS.map(p => { if (p.id === 'custom') { return { ...p, name: customPreset.name, description: customPreset.description }; } return p; }); const handleSaveSettings = () => { const currentPreset = INITIAL_PERSONA_PRESETS.find(p => p.description === aiSettings.persona && p.id !== 'custom'); if (!currentPreset) { setCustomPreset({ name: aiSettings.name, description: aiSettings.persona }); } setIsSettingsOpen(false); }; return (
{/* Chat Tab */}
setIsSettingsOpen(true)}>
{aiSettings.avatar ? avatar : aiSettings.name.charAt(0)}

{aiSettings.name}

{aiSettings.persona.split(/[。!]/)[0] + '...'}
今日支出 ¥{todaySpending.toFixed(0)}
{/* AI Settings Modal */} {isSettingsOpen && (

管家设定

{aiSettings.avatar ? avatar : aiSettings.name.charAt(0)}
更改管家形象
setAiSettings({...aiSettings, name: e.target.value})} className="w-full p-3.5 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none font-bold text-gray-800" />
{personaPresets.map(p => ( ))}