import { useState, useRef, useEffect } from 'react'; import { Paper, Text, Stack, Group, TextInput, ActionIcon, Loader, Box, Transition, Button, Badge, Divider, } from '@mantine/core'; import { IconSparkles, IconSend, IconCalendar, IconRepeat, IconFlag, } from '@tabler/icons-react'; import { api } from '../../services/api'; import { useAppStore } from '../../stores'; import type { AIConversation, AIParsedEvent, RepeatType, PriorityType } from '../../types'; import { notifications } from '@mantine/notifications'; interface FloatingAIChatProps { onEventCreated?: () => void; hidden?: boolean; } export function FloatingAIChat({ onEventCreated, hidden = false }: FloatingAIChatProps) { const [isFocused, setIsFocused] = useState(false); const [message, setMessage] = useState(''); const [loading, setLoading] = useState(false); const [history, setHistory] = useState([]); const [parsedEvent, setParsedEvent] = useState(null); const [showPreview, setShowPreview] = useState(false); const addConversation = useAppStore((state) => state.addConversation); const scrollRef = useRef(null); const inputRef = useRef(null); // 编辑状态 const [editForm, setEditForm] = useState({ title: '', date: '', timezone: 'Asia/Shanghai', is_lunar: false, repeat_type: 'none', type: 'reminder', }); const scrollToBottom = () => { scrollRef.current?.scrollIntoView({ behavior: 'smooth' }); }; useEffect(() => { scrollToBottom(); }, [history, showPreview, loading]); // 聚焦时展开 const handleFocus = () => { setIsFocused(true); }; // 点击遮罩层关闭 const handleClose = () => { setIsFocused(false); setShowPreview(false); setParsedEvent(null); }; const handleSend = async () => { if (!message.trim() || loading) return; const userMessage = message.trim(); setMessage(''); setLoading(true); setShowPreview(false); try { const result = await api.ai.parse(userMessage); // 保存解析结果用于预览 if (result.parsed) { setParsedEvent(result.parsed as AIParsedEvent); setEditForm(result.parsed as AIParsedEvent); setShowPreview(true); } // 确保只保存response文本 const responseText = typeof result.response === 'string' ? result.response : '已为你创建提醒'; const newConversation: AIConversation = { id: Date.now().toString(), user_id: '', message: userMessage, response: responseText, created_at: new Date().toISOString(), }; setHistory((prev) => [...prev, newConversation]); addConversation(newConversation); if (onEventCreated) { onEventCreated(); } } catch (error) { console.error('AI parse error:', error); notifications.show({ title: '解析失败', message: 'AI 解析出错,请重试', color: 'red', }); } finally { setLoading(false); } }; const handleConfirm = async () => { if (!parsedEvent) return; try { const eventData = { type: editForm.type, title: editForm.title, date: editForm.date, is_lunar: editForm.is_lunar, repeat_type: editForm.repeat_type, content: editForm.content, is_holiday: editForm.is_holiday, priority: editForm.priority, reminder_times: editForm.reminder_times, }; await api.events.create(eventData); notifications.show({ title: editForm.type === 'anniversary' ? '纪念日已创建' : '提醒已创建', message: editForm.title, color: 'green', }); setShowPreview(false); setParsedEvent(null); setIsFocused(false); setHistory([]); setMessage(''); if (onEventCreated) { onEventCreated(); } } catch (error) { console.error('Create event error:', error); notifications.show({ title: '创建失败', message: '请重试', color: 'red', }); } }; const handleCancel = () => { setShowPreview(false); setParsedEvent(null); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } if (e.key === 'Escape' && isFocused) { handleClose(); } }; // 获取日期显示文本 const getDateDisplay = () => { if (!editForm.date) return '未设置'; const timezone = editForm.timezone || 'Asia/Shanghai'; const dateStr = editForm.date; const utcDate = new Date(dateStr); if (isNaN(utcDate.getTime())) return '日期格式错误'; const localDateStr = utcDate.toLocaleString('en-US', { timeZone: timezone }); const localDate = new Date(localDateStr); const hours = localDate.getHours(); const minutes = localDate.getMinutes(); const hasExplicitTime = hours !== 0 || minutes !== 0; if (hasExplicitTime) { return localDate.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false, timeZone: timezone, }); } return `${localDate.getFullYear()}-${String(localDate.getMonth() + 1).padStart(2, '0')}-${String(localDate.getDate()).padStart(2, '0')}`; }; const getTypeDisplay = () => { return editForm.type === 'anniversary' ? '纪念日' : '提醒'; }; // 当 hidden 为 true 时,不渲染组件 if (hidden) { return null; } return ( <> {/* 遮罩层 */} {(styles) => ( )} {/* 悬浮容器 - 聚焦时放大 */} {/* 展开的聊天面板 */} {(panelStyles) => ( {/* Header */} AI 助手 {/* Chat area */} {history.length === 0 && !showPreview && !loading ? ( 告诉 AI 帮你添加纪念日或提醒 例如:「下周三见客户」「每月15号还房贷」「明年5月20日结婚纪念日」 ) : ( <> {history.map((conv) => ( {conv.message} AI {conv.response} ))} {loading && ( AI 正在思考... )} {showPreview && parsedEvent && ( {getTypeDisplay()} {editForm.title || '未设置'} 日期 {getDateDisplay()} 重复 {['none', 'daily', 'weekly', 'monthly', 'yearly'].map((type) => ( ))} {editForm.type === 'reminder' && ( 颜色 {[ { value: 'none', color: 'var(--mantine-color-gray-3)' }, { value: 'red', color: '#ef4444' }, { value: 'green', color: '#22c55e' }, { value: 'yellow', color: '#eab308' }, ].map((item) => ( setEditForm({ ...editForm, priority: item.value as PriorityType })} style={{ width: 16, height: 16, borderRadius: '50%', background: item.color, border: editForm.priority === item.value ? '2px solid var(--mantine-color-dark-6)' : '1px solid var(--mantine-color-gray-2)', cursor: 'pointer', }} /> ))} )} )} )}
)} {/* 输入框区域 */} setMessage(e.target.value)} onKeyDown={handleKeyDown} onFocus={handleFocus} placeholder="输入你想记住的事情..." size="sm" style={{ flex: 1 }} disabled={loading} styles={{ input: { borderRadius: 12, borderColor: 'var(--mantine-color-gray-3)', background: isFocused ? 'rgba(255, 255, 255, 0.95)' : 'rgba(0, 0, 0, 0.06)', color: '#1a1a1a', paddingLeft: 12, paddingRight: 12, transition: 'all 0.3s ease', }, }} /> ); }