feat: 优化提醒卡片样式和列表滚动功能

- 调整正常提醒卡片布局:左侧 checkbox+标题+内容,右侧日期时间
- 移除正常卡片悬停时的编辑按钮
- 纪念日/提醒/便签列表支持独立滚动
- 页面整体禁用滚动,支持 Ctrl+滚轮缩放
- 滚动条样式优化:默认隐藏,悬停时显示淡雅样式

Co-Authored-By: Claude (MiniMax-M2.1) <noreply@anthropic.com>
This commit is contained in:
ddshi 2026-02-03 16:29:25 +08:00
parent 7b0afbb27b
commit 3fdee5cab4
5 changed files with 348 additions and 142 deletions

View File

@ -1,5 +1,5 @@
import { useMemo } from 'react'; import { useMemo, useRef } from 'react';
import { Stack, Text, Paper, Group, Button } from '@mantine/core'; import { Stack, Text, Paper, Group, Button, Box } from '@mantine/core';
import { IconPlus } from '@tabler/icons-react'; import { IconPlus } from '@tabler/icons-react';
import { AnniversaryCard } from './AnniversaryCard'; import { AnniversaryCard } from './AnniversaryCard';
import { getHolidaysForYear } from '../../constants/holidays'; import { getHolidaysForYear } from '../../constants/holidays';
@ -28,6 +28,42 @@ interface BuiltInHolidayEvent {
export function AnniversaryList({ events, onEventClick, onAddClick }: AnniversaryListProps) { export function AnniversaryList({ events, onEventClick, onAddClick }: AnniversaryListProps) {
const anniversaries = events.filter((e) => e.type === 'anniversary'); const anniversaries = events.filter((e) => e.type === 'anniversary');
const showHolidays = useAppStore((state) => state.settings?.showHolidays ?? true); const showHolidays = useAppStore((state) => state.settings?.showHolidays ?? true);
const scrollContainerRef = useRef<HTMLDivElement>(null);
// 滚动条样式 - 仅在悬停时显示
const scrollbarStyle = `
.anniversary-scroll::-webkit-scrollbar {
width: 4px;
height: 4px;
}
.anniversary-scroll::-webkit-scrollbar-track {
background: transparent;
}
.anniversary-scroll::-webkit-scrollbar-thumb {
background: transparent;
border-radius: 2px;
}
.anniversary-scroll:hover::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.15);
}
`;
// 处理滚轮事件,实现列表独立滚动
const handleWheel = (e: React.WheelEvent) => {
const container = scrollContainerRef.current;
if (!container) return;
const { scrollTop, scrollHeight, clientHeight } = container;
// 使用 1px 缓冲避免浮点数精度问题
const isAtTop = scrollTop <= 0;
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 1;
// 如果已经滚动到顶部且向下滚动,或者已经滚动到底部且向上滚动,则阻止事件冒泡
if ((isAtTop && e.deltaY > 0) || (isAtBottom && e.deltaY < 0)) {
e.stopPropagation();
}
// 如果在滚动范围内,允许事件继续传递以实现正常滚动
};
// 获取内置节假日 // 获取内置节假日
const builtInHolidays = useMemo(() => { const builtInHolidays = useMemo(() => {
@ -88,8 +124,9 @@ export function AnniversaryList({ events, onEventClick, onAddClick }: Anniversar
// 空状态 // 空状态
if (allAnniversaries.total === 0) { if (allAnniversaries.total === 0) {
return ( return (
<Paper p="md" withBorder radius={4} h="100%"> <Paper p="md" withBorder radius={4} style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<Stack align="center" justify="center" h="100%"> <Stack align="center" justify="center" style={{ flex: 1 }}>
<style>{scrollbarStyle}</style>
<Text c="#999" size="sm" style={{ letterSpacing: '0.05em' }}> <Text c="#999" size="sm" style={{ letterSpacing: '0.05em' }}>
</Text> </Text>
@ -112,8 +149,10 @@ export function AnniversaryList({ events, onEventClick, onAddClick }: Anniversar
} }
return ( return (
<Paper p="md" withBorder radius={4} h="100%"> <>
<Group justify="space-between" mb="sm"> <style>{scrollbarStyle}</style>
<Paper p="md" withBorder radius={4} data-scroll-container="anniversary" style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<Group justify="space-between" mb="sm" style={{ flexShrink: 0 }}>
<Group gap={8}> <Group gap={8}>
<Text fw={500} size="sm" style={{ letterSpacing: '0.1em', color: '#1a1a1a' }}> <Text fw={500} size="sm" style={{ letterSpacing: '0.1em', color: '#1a1a1a' }}>
@ -136,7 +175,13 @@ export function AnniversaryList({ events, onEventClick, onAddClick }: Anniversar
</Button> </Button>
</Group> </Group>
<Stack gap="xs" style={{ maxHeight: 'calc(100% - 40px)', overflowY: 'auto' }}> <Box
ref={scrollContainerRef}
onWheel={handleWheel}
className="anniversary-scroll"
style={{ flex: 1, overflowY: 'auto', minHeight: 0 }}
>
<Stack gap="xs">
{/* 内置节假日 */} {/* 内置节假日 */}
{allAnniversaries.builtIn.length > 0 && ( {allAnniversaries.builtIn.length > 0 && (
<> <>
@ -193,7 +238,9 @@ export function AnniversaryList({ events, onEventClick, onAddClick }: Anniversar
))} ))}
</> </>
)} )}
</Stack> </Stack>
</Paper> </Box>
</Paper>
</>
); );
} }

View File

@ -1,4 +1,4 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback, useRef } from 'react';
import { import {
Paper, Paper,
Textarea, Textarea,
@ -47,6 +47,24 @@ export function NoteEditor({ onSave }: NoteEditorProps) {
// Auto-save with 3 second debounce // Auto-save with 3 second debounce
const [debouncedContent] = useDebouncedValue(content, 3000); const [debouncedContent] = useDebouncedValue(content, 3000);
// 滚动条样式 - 仅在悬停时显示
const scrollbarStyle = `
.note-scroll::-webkit-scrollbar {
width: 4px;
height: 4px;
}
.note-scroll::-webkit-scrollbar-track {
background: transparent;
}
.note-scroll::-webkit-scrollbar-thumb {
background: transparent;
border-radius: 2px;
}
.note-scroll:hover::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.15);
}
`;
useEffect(() => { useEffect(() => {
if (debouncedContent !== undefined && notes && debouncedContent !== content) { if (debouncedContent !== undefined && notes && debouncedContent !== content) {
handleSave(debouncedContent); handleSave(debouncedContent);
@ -87,8 +105,28 @@ export function NoteEditor({ onSave }: NoteEditorProps) {
} }
}; };
const scrollContainerRef = useRef<HTMLDivElement>(null);
// 处理滚轮事件,实现列表独立滚动
const handleWheel = (e: React.WheelEvent) => {
const container = scrollContainerRef.current;
if (!container) return;
const { scrollTop, scrollHeight, clientHeight } = container;
// 使用 1px 缓冲避免浮点数精度问题
const isAtTop = scrollTop <= 0;
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 1;
// 如果已经滚动到顶部且向下滚动,或者已经滚动到底部且向上滚动,则阻止事件冒泡
if ((isAtTop && e.deltaY > 0) || (isAtBottom && e.deltaY < 0)) {
e.stopPropagation();
}
// 如果在滚动范围内,允许事件继续传递以实现正常滚动
};
return ( return (
<Paper p="md" withBorder radius={4} h="100%" style={{ display: 'flex', flexDirection: 'column' }}> <Paper p="md" withBorder radius={4} style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<style>{scrollbarStyle}</style>
<Stack gap="sm" style={{ flex: 1, display: 'flex', flexDirection: 'column' }}> <Stack gap="sm" style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
{/* Header */} {/* Header */}
<Group justify="space-between" style={{ flexShrink: 0 }}> <Group justify="space-between" style={{ flexShrink: 0 }}>
@ -119,7 +157,13 @@ export function NoteEditor({ onSave }: NoteEditorProps) {
</Group> </Group>
{/* Editor/Preview Area */} {/* Editor/Preview Area */}
<Box style={{ flex: 1, minHeight: 0, display: 'flex' }}> <Box
ref={scrollContainerRef}
onWheel={handleWheel}
data-scroll-container="note"
className="note-scroll"
style={{ flex: 1, minHeight: 0, display: 'flex', overflowY: 'auto' }}
>
{viewMode === 'edit' && ( {viewMode === 'edit' && (
<Textarea <Textarea
value={content} value={content}
@ -138,7 +182,11 @@ export function NoteEditor({ onSave }: NoteEditorProps) {
'&:focus': { outline: 'none' }, '&:focus': { outline: 'none' },
}, },
}} }}
style={{ flex: 1, width: '100%' }} style={{
flex: 1,
width: '100%',
overflowY: 'auto',
}}
/> />
)} )}

View File

@ -1,5 +1,5 @@
import { useMemo, useState, useEffect } from 'react'; import { useMemo, useState } from 'react';
import { Paper, Text, Checkbox, Group, Stack, ActionIcon } from '@mantine/core'; import { Paper, Text, Checkbox, Group, Stack, ActionIcon, Box } from '@mantine/core';
import { IconDots, IconArrowForward } from '@tabler/icons-react'; import { IconDots, IconArrowForward } from '@tabler/icons-react';
import type { Event } from '../../types'; import type { Event } from '../../types';
@ -129,68 +129,70 @@ export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, i
100% { opacity: 0; transform: translateX(20px); } 100% { opacity: 0; transform: translateX(20px); }
} }
`}</style> `}</style>
<Group justify="space-between" wrap="nowrap"> {/* 正常提醒卡片:左右分栏布局 */}
<Group gap="sm" style={{ flex: 1, minWidth: 0 }}> {!isMissed ? (
{/* Checkbox */} <Group justify="space-between" wrap="nowrap" align="flex-start">
<div {/* 左侧Checkbox + 标题 + 内容 */}
style={{ <Box
display: 'flex', style={{ flex: 1, minWidth: 0, cursor: 'pointer' }}
alignItems: 'center', onClick={(e) => {
animation: isAnimating ? 'reminder-card-pulse 0.3s ease-out' : 'none', // 阻止事件冒泡到 Card避免重复触发
e.stopPropagation();
onClick();
}} }}
> >
<Checkbox <Group gap="sm" wrap="nowrap" align="flex-start">
checked={isCompleted} {/* Checkbox - 单独处理,不触发卡片点击 */}
onChange={(e) => { <div
e.stopPropagation(); style={{
if (isMissed && !isCompleted) { display: 'flex',
// 已过期提醒:先播放动画,动画结束后再触发 toggle alignItems: 'flex-start',
setIsAnimating(true); paddingTop: 2,
setTimeout(() => { animation: isAnimating ? 'reminder-card-pulse 0.3s ease-out' : 'none',
setIsAnimating(false); }}
onClick={(e) => {
// 阻止事件冒泡,避免触发卡片点击
e.stopPropagation();
}}
>
<Checkbox
checked={isCompleted}
onChange={(e) => {
e.stopPropagation();
onToggle(); onToggle();
}, 300); }}
} else { size="xs"
onToggle(); color="#1a1a1a"
} style={{
}} animation: isAnimating ? 'reminder-card-pulse 0.3s ease-out' : 'none',
size="xs" }}
color="#1a1a1a" />
/> </div>
</div>
<Stack gap={2} style={{ flex: 1, minWidth: 0 }} onClick={onClick}> {/* 标题 */}
{/* Title */} <Text
<Text fw={400}
fw={400} size="xs"
size="xs" lineClamp={1}
lineClamp={1} style={{
style={{ textDecoration: isCompleted ? 'line-through' : 'none',
textDecoration: isCompleted ? 'line-through' : 'none', color: getTextColor(),
color: getTextColor(), letterSpacing: '0.03em',
letterSpacing: '0.03em', transition: 'color 0.2s ease',
transition: 'color 0.2s ease', }}
}} >
> {event.title}
{event.title} </Text>
</Text> </Group>
{/* Time */} {/* 内容在标题下方 */}
<Text
size="xs"
c={getTimeColor()}
style={{ letterSpacing: '0.05em' }}
>
{timeInfo.timeStr}
</Text>
{/* Content preview - 始终显示 */}
{event.content && ( {event.content && (
<Text <Text
size="xs" size="xs"
c={isCompleted ? '#bbb' : '#999'} c={isCompleted ? '#bbb' : '#999'}
lineClamp={1} lineClamp={1}
style={{ style={{
marginLeft: 28,
opacity: isCompleted ? 0.6 : 1, opacity: isCompleted ? 0.6 : 1,
transition: 'opacity 0.2s ease', transition: 'opacity 0.2s ease',
}} }}
@ -198,50 +200,126 @@ export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, i
{event.content} {event.content}
</Text> </Text>
)} )}
</Stack> </Box>
</Group>
{/* Quick actions */} {/* 右侧:日期时间 */}
<Group gap={4}> <Box style={{ flex: '0 0 auto', minWidth: 0, paddingLeft: 12 }}>
{/* 顺延按钮 - 仅在逾期状态显示 */} <Text
{isMissed && onPostpone && ( size="xs"
c={getTimeColor()}
style={{ letterSpacing: '0.05em', whiteSpace: 'nowrap' }}
>
{timeInfo.timeStr}
</Text>
</Box>
</Group>
) : (
/* 逾期提醒卡片:保持原有结构 */
<Group justify="space-between" wrap="nowrap">
<Group gap="sm" style={{ flex: 1, minWidth: 0 }}>
<div
style={{
display: 'flex',
alignItems: 'center',
animation: isAnimating ? 'reminder-card-pulse 0.3s ease-out' : 'none',
}}
>
<Checkbox
checked={isCompleted}
onChange={(e) => {
e.stopPropagation();
if (isMissed && !isCompleted) {
setIsAnimating(true);
setTimeout(() => {
setIsAnimating(false);
onToggle();
}, 300);
} else {
onToggle();
}
}}
size="xs"
color="#1a1a1a"
/>
</div>
<Stack gap={2} style={{ flex: 1, minWidth: 0 }} onClick={onClick}>
<Text
fw={400}
size="xs"
lineClamp={1}
style={{
textDecoration: isCompleted ? 'line-through' : 'none',
color: getTextColor(),
letterSpacing: '0.03em',
transition: 'color 0.2s ease',
}}
>
{event.title}
</Text>
<Text
size="xs"
c={getTimeColor()}
style={{ letterSpacing: '0.05em' }}
>
{timeInfo.timeStr}
</Text>
{event.content && (
<Text
size="xs"
c={isCompleted ? '#bbb' : '#999'}
lineClamp={1}
style={{
opacity: isCompleted ? 0.6 : 1,
transition: 'opacity 0.2s ease',
}}
>
{event.content}
</Text>
)}
</Stack>
</Group>
<Group gap={4}>
{onPostpone && (
<ActionIcon
size="sm"
variant="subtle"
color="orange"
onClick={(e) => {
e.stopPropagation();
onPostpone();
}}
style={{
color: '#e67e22',
opacity: isHovered ? 1 : 0,
transition: 'all 0.2s ease',
}}
title="顺延到今天"
>
<IconArrowForward size={12} />
</ActionIcon>
)}
<ActionIcon <ActionIcon
size="sm" size="sm"
variant="subtle" variant="subtle"
color="orange" color="gray"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onPostpone(); onClick();
}} }}
style={{ style={{
color: '#e67e22', color: '#999',
opacity: isHovered ? 1 : 0, opacity: isHovered ? 1 : 0,
transition: 'all 0.2s ease', transition: 'all 0.2s ease',
}} }}
title="顺延到今天" title="编辑"
> >
<IconArrowForward size={12} /> <IconDots size={12} />
</ActionIcon> </ActionIcon>
)} </Group>
<ActionIcon
size="sm"
variant="subtle"
color="gray"
onClick={(e) => {
e.stopPropagation();
onClick();
}}
style={{
color: '#999',
opacity: isHovered ? 1 : 0,
transition: 'all 0.2s ease',
}}
title="编辑"
>
<IconDots size={12} />
</ActionIcon>
</Group> </Group>
</Group> )}
</Paper> </Paper>
); );
} }

View File

@ -1,4 +1,4 @@
import { useMemo } from 'react'; import { useMemo, useRef } from 'react';
import { import {
Stack, Stack,
Text, Text,
@ -6,6 +6,7 @@ import {
Group, Group,
Button, Button,
Alert, Alert,
Box,
} from '@mantine/core'; } from '@mantine/core';
import { import {
IconPlus, IconPlus,
@ -20,7 +21,6 @@ interface ReminderListProps {
onToggleComplete: (event: Event) => void; onToggleComplete: (event: Event) => void;
onAddClick: () => void; onAddClick: () => void;
onDelete?: (event: Event) => void; onDelete?: (event: Event) => void;
onRestore?: (event: Event) => void;
onPostpone?: (event: Event) => void; onPostpone?: (event: Event) => void;
} }
@ -30,9 +30,44 @@ export function ReminderList({
onToggleComplete, onToggleComplete,
onAddClick, onAddClick,
onDelete, onDelete,
onRestore,
onPostpone, onPostpone,
}: ReminderListProps) { }: ReminderListProps) {
const scrollContainerRef = useRef<HTMLDivElement>(null);
// 滚动条样式 - 仅在悬停时显示
const scrollbarStyle = `
.reminder-scroll::-webkit-scrollbar {
width: 4px;
height: 4px;
}
.reminder-scroll::-webkit-scrollbar-track {
background: transparent;
}
.reminder-scroll::-webkit-scrollbar-thumb {
background: transparent;
border-radius: 2px;
}
.reminder-scroll:hover::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.15);
}
`;
// 处理滚轮事件,实现列表独立滚动
const handleWheel = (e: React.WheelEvent) => {
const container = scrollContainerRef.current;
if (!container) return;
const { scrollTop, scrollHeight, clientHeight } = container;
// 使用 1px 缓冲避免浮点数精度问题
const isAtTop = scrollTop <= 0;
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 1;
// 如果已经滚动到顶部且向下滚动,或者已经滚动到底部且向上滚动,则阻止事件冒泡
if ((isAtTop && e.deltaY > 0) || (isAtBottom && e.deltaY < 0)) {
e.stopPropagation();
}
// 如果在滚动范围内,允许事件继续传递以实现正常滚动
};
const grouped = useMemo(() => { const grouped = useMemo(() => {
const now = new Date(); const now = new Date();
@ -109,8 +144,8 @@ export function ReminderList({
// 空状态 // 空状态
if (!hasActiveReminders) { if (!hasActiveReminders) {
return ( return (
<Paper p="md" withBorder radius={4} h="100%"> <Paper p="md" withBorder radius={4} style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<Stack align="center" justify="center" h="100%"> <Stack align="center" justify="center" style={{ flex: 1 }}>
<Text c="#999" size="sm" ta="center" style={{ letterSpacing: '0.05em' }}> <Text c="#999" size="sm" ta="center" style={{ letterSpacing: '0.05em' }}>
</Text> </Text>
@ -141,7 +176,8 @@ export function ReminderList({
return ( return (
<> <>
<Paper p="md" withBorder radius={4} h="100%" style={{ display: 'flex', flexDirection: 'column' }}> <style>{scrollbarStyle}</style>
<Paper p="md" withBorder radius={4} data-scroll-container="reminder" style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
{/* Header */} {/* Header */}
<Group justify="space-between" mb="sm" style={{ flexShrink: 0 }}> <Group justify="space-between" mb="sm" style={{ flexShrink: 0 }}>
<Group gap={8}> <Group gap={8}>
@ -169,7 +205,17 @@ export function ReminderList({
</Group> </Group>
{/* Content */} {/* Content */}
<Stack gap="xs" style={{ flex: 1, overflowY: 'auto', minHeight: 0 }}> <Box
ref={scrollContainerRef}
onWheel={handleWheel}
className="reminder-scroll"
style={{
flex: 1,
overflowY: 'auto',
minHeight: 0,
}}
>
<Stack gap="xs">
{/* 逾期提醒 */} {/* 逾期提醒 */}
{grouped.missed.length > 0 && ( {grouped.missed.length > 0 && (
<Alert <Alert
@ -260,6 +306,7 @@ export function ReminderList({
</> </>
)} )}
</Stack> </Stack>
</Box>
</Paper> </Paper>
</> </>
); );

View File

@ -1,7 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { import {
Container, Container,
Grid,
Title, Title,
Button, Button,
Group, Group,
@ -159,14 +158,6 @@ export function HomePage() {
fetchEvents(); fetchEvents();
}; };
const handleRestore = async (event: Event) => {
if (event.type !== 'reminder') return;
await updateEventById(event.id, {
is_completed: false,
});
fetchEvents();
};
const handlePostpone = async (event: Event) => { const handlePostpone = async (event: Event) => {
if (event.type !== 'reminder') return; if (event.type !== 'reminder') return;
// 将日期顺延到今天,保留原事件的时间 // 将日期顺延到今天,保留原事件的时间
@ -210,9 +201,11 @@ export function HomePage() {
style={{ style={{
minHeight: '100vh', minHeight: '100vh',
background: '#faf9f7', background: '#faf9f7',
overflow: 'hidden',
height: '100vh',
}} }}
> >
<Container size="xl" py="md" h="100vh" style={{ display: 'flex', flexDirection: 'column' }}> <Container size="xl" py="md" h="100vh" style={{ display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
{/* Header */} {/* Header */}
<Group justify="space-between" mb="md" style={{ flexShrink: 0 }}> <Group justify="space-between" mb="md" style={{ flexShrink: 0 }}>
<Title <Title
@ -279,40 +272,33 @@ export function HomePage() {
</Group> </Group>
{/* Main Content - 3 column horizontal layout */} {/* Main Content - 3 column horizontal layout */}
<Grid grow style={{ flex: 1, minHeight: 0 }} gutter="md"> <div style={{ flex: 1, minHeight: 0, display: 'flex', gap: 16, overflow: 'hidden' }}>
{/* Left column - Anniversary */} {/* Left column - Anniversary */}
<Grid.Col span={4}> <div style={{ flex: 1, minWidth: 0, height: '100%', overflow: 'hidden' }}>
<div style={{ height: '100%', minHeight: 0 }}> <AnniversaryList
<AnniversaryList events={events}
events={events} onEventClick={handleEventClick}
onEventClick={handleEventClick} onAddClick={() => handleAddClick('anniversary')}
onAddClick={() => handleAddClick('anniversary')} />
/> </div>
</div>
</Grid.Col>
{/* Middle column - Reminder */} {/* Middle column - Reminder */}
<Grid.Col span={4}> <div style={{ flex: 1, minWidth: 0, height: '100%', overflow: 'hidden' }}>
<div style={{ height: '100%', minHeight: 0 }}> <ReminderList
<ReminderList events={events}
events={events} onEventClick={handleEventClick}
onEventClick={handleEventClick} onToggleComplete={handleToggleComplete}
onToggleComplete={handleToggleComplete} onAddClick={() => handleAddClick('reminder')}
onAddClick={() => handleAddClick('reminder')} onDelete={handleDelete}
onDelete={handleDelete} onPostpone={handlePostpone}
onRestore={handleRestore} />
onPostpone={handlePostpone} </div>
/>
</div>
</Grid.Col>
{/* Right column - Note */} {/* Right column - Note */}
<Grid.Col span={4}> <div style={{ flex: 1, minWidth: 0, height: '100%', overflow: 'hidden' }}>
<div style={{ height: '100%', minHeight: 0 }}> <NoteEditor />
<NoteEditor /> </div>
</div> </div>
</Grid.Col>
</Grid>
{/* AI Chat - Floating */} {/* AI Chat - Floating */}
<FloatingAIChat onEventCreated={handleAIEventCreated} /> <FloatingAIChat onEventCreated={handleAIEventCreated} />