Files
Momento/architectural-grid11/src/App.tsx
Antigravity 1fcea6ed7d
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 7s
feat: brainstorm sessions, PDF document Q&A, embedding fixes, and UI improvements
- Add brainstorm feature with collaborative canvas, AI idea generation, live cursors, playback, and export
- Add PDF upload/extraction/ingestion pipeline with pgvector document search (RAG)
- Add document Q&A overlay with streaming chat and PDF preview
- Add note attachments UI with status polling, grid layout, and auto-scroll
- Add task extraction AI tool and agent executor improvements
- Fix NoteEmbedding missing updatedAt column, re-index 66 notes with 1536-dim embeddings
- Fix brainstorm 'Create Note' button: add success toast and redirect to created note
- Fix memory echo notification infinite polling
- Fix chat route to always include document_search tool
- Add brainstorm i18n keys across all 14 locales
- Add socket server for real-time brainstorm collaboration
- Add hierarchical notebook selector and organize notebook dialog improvements
- Add sidebar brainstorm section with session management
- Update prisma schema with brainstorm tables, attachments, and document chunks
2026-05-14 17:43:21 +00:00

604 lines
25 KiB
TypeScript

/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
import React, { useState, useMemo } from 'react';
import { motion, AnimatePresence } from 'motion/react';
// Components
import { Sidebar } from './components/Sidebar';
import { NotebooksView } from './components/NotebooksView';
import { AgentsView } from './components/AgentsView';
import { SettingsView } from './components/SettingsView';
import { TrashView } from './components/TrashView';
import { BrainstormView } from './components/BrainstormView/BrainstormView';
import { InsightsView } from './components/InsightsView';
import { TemporalView } from './components/TemporalView';
import { AISidebar } from './components/AISidebar';
import { SlashMenu } from './components/SlashMenu';
// Data & Types
import { CARNETS, ALL_NOTES } from './constants';
import { NavigationView, SettingsTab, AITab, AITone, Carnet, Note, BrainstormIdea, NoteAccessLog } from './types';
export default function App() {
const [activeView, setActiveView] = useState<NavigationView>('notebooks');
const [activeSettingsTab, setActiveSettingsTab] = useState<SettingsTab>('general');
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
const [isDarkMode, setIsDarkMode] = useState(false);
const [carnets, setCarnets] = useState<Carnet[]>(CARNETS);
const [notes, setNotes] = useState<Note[]>(ALL_NOTES);
const [accessLogs, setAccessLogs] = useState<NoteAccessLog[]>([
// Note n1: 14-day cycle
{ noteId: 'n1', accessedAt: new Date(Date.now() - 70 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
{ noteId: 'n1', accessedAt: new Date(Date.now() - 56 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
{ noteId: 'n1', accessedAt: new Date(Date.now() - 42 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
{ noteId: 'n1', accessedAt: new Date(Date.now() - 28 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
{ noteId: 'n1', accessedAt: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
{ noteId: 'n1', accessedAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
// Note n2: 7-day cycle
{ noteId: 'n2', accessedAt: new Date(Date.now() - 35 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
{ noteId: 'n2', accessedAt: new Date(Date.now() - 28 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
{ noteId: 'n2', accessedAt: new Date(Date.now() - 21 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
{ noteId: 'n2', accessedAt: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
{ noteId: 'n2', accessedAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
{ noteId: 'n2', accessedAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
// Note n3: 3-day cycle (frequent check)
{ noteId: 'n3', accessedAt: new Date(Date.now() - 12 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
{ noteId: 'n3', accessedAt: new Date(Date.now() - 9 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
{ noteId: 'n3', accessedAt: new Date(Date.now() - 6 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
{ noteId: 'n3', accessedAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
{ noteId: 'n3', accessedAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(), action: 'view' },
]);
const logNoteAccess = (noteId: string, action: 'view' | 'edit' | 'search_hit' = 'view') => {
const newLog: NoteAccessLog = {
noteId,
accessedAt: new Date().toISOString(),
action
};
setAccessLogs(prev => [...prev, newLog]);
};
const [activeCarnetId, setActiveCarnetId] = useState('4');
const [activeNoteId, setActiveNoteId] = useState<string | null>(null);
const [brainstormSeed, setBrainstormSeed] = useState<string | null>(null);
const handleBrainstormNote = (note: Note) => {
setActiveView('brainstorm');
// We'll use a small delay or a ref to pass this to BrainstormView if needed,
// but better to just share state or use a CustomEvent
window.dispatchEvent(new CustomEvent('start-brainstorm', {
detail: { seed: note.title, sourceNoteId: note.id }
}));
};
React.useEffect(() => {
if (activeNoteId) {
logNoteAccess(activeNoteId);
}
}, [activeNoteId]);
React.useEffect(() => {
const handleSwitchView = (e: any) => {
if (e.detail) {
setActiveView(e.detail as NavigationView);
}
};
window.addEventListener('switch-view', handleSwitchView);
return () => window.removeEventListener('switch-view', handleSwitchView);
}, []);
const [selectedTagIds, setSelectedTagIds] = useState<string[]>([]);
const [isAISidebarOpen, setIsAISidebarOpen] = useState(false);
const [aiTab, setAiTab] = useState<AITab>('discussion');
const [selectedTone, setSelectedTone] = useState<AITone>('Professional');
// Modal States
const [showNewCarnetModal, setShowNewCarnetModal] = useState<{ isOpen: boolean; parentId?: string; isRenaming?: boolean; carnetId?: string }>({ isOpen: false });
const [showNewNoteModal, setShowNewNoteModal] = useState(false);
const [slashMenu, setSlashMenu] = useState<{ isOpen: boolean; top: number; left: number } | null>(null);
// Form States
const [newCarnetName, setNewCarnetName] = useState('');
const [newNoteTitle, setNewNoteTitle] = useState('');
const [newNoteContent, setNewNoteContent] = useState('');
const handleEditorKeyDown = (e: React.KeyboardEvent) => {
if (e.key === '/') {
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
setSlashMenu({
isOpen: true,
top: rect.bottom + window.scrollY,
left: rect.left + window.scrollX
});
}
}
};
const togglePin = (noteId: string) => {
setNotes(notes.map(n => n.id === noteId ? { ...n, isPinned: !n.isPinned } : n));
};
const filteredNotes = useMemo(() => {
let result = notes.filter(n => n.carnetId === activeCarnetId && !n.isDeleted);
if (selectedTagIds.length > 0) {
result = result.filter(note =>
selectedTagIds.every(tagId => note.tags?.some(tag => tag.id === tagId))
);
}
return [...result].sort((a, b) => {
if (a.isPinned && !b.isPinned) return -1;
if (!a.isPinned && b.isPinned) return 1;
return 0;
});
}, [activeCarnetId, notes]);
const activeNote = useMemo(() =>
notes.find(n => n.id === activeNoteId),
[activeNoteId, notes]);
const activeCarnet = useMemo(() =>
carnets.find(c => c.id === activeCarnetId),
[activeCarnetId, carnets]);
const handleAddCarnet = (e: React.FormEvent) => {
e.preventDefault();
if (!newCarnetName.trim()) return;
if (showNewCarnetModal.isRenaming && showNewCarnetModal.carnetId) {
setCarnets(carnets.map(c => c.id === showNewCarnetModal.carnetId ? { ...c, name: newCarnetName, initial: newCarnetName.charAt(0).toUpperCase() } : c));
setShowNewCarnetModal({ isOpen: false });
setNewCarnetName('');
return;
}
const newCarnet: Carnet = {
id: Date.now().toString(),
name: newCarnetName,
initial: newCarnetName.charAt(0).toUpperCase(),
type: 'Project',
parentId: showNewCarnetModal.parentId
};
setCarnets([...carnets, newCarnet]);
setNewCarnetName('');
setShowNewCarnetModal({ isOpen: false });
setActiveCarnetId(newCarnet.id);
};
const handleDeleteCarnet = (id: string) => {
if (window.confirm('Déplacer ce carnet et ses sous-carnets vers la corbeille ?')) {
const idsToDelete = new Set<string>([id]);
const addChildren = (parentId: string) => {
carnets.forEach(c => {
if (c.parentId === parentId) {
idsToDelete.add(c.id);
addChildren(c.id);
}
});
};
addChildren(id);
const deletedAt = new Date().toISOString();
setCarnets(carnets.map(c => idsToDelete.has(c.id) ? { ...c, isDeleted: true, deletedAt } : c));
setNotes(notes.map(n => idsToDelete.has(n.carnetId) ? { ...n, isDeleted: true, deletedAt } : n));
if (idsToDelete.has(activeCarnetId)) {
setActiveCarnetId('1');
}
}
};
const handleDeleteNote = (id: string) => {
const deletedAt = new Date().toISOString();
setNotes(notes.map(n => n.id === id ? { ...n, isDeleted: true, deletedAt } : n));
if (activeNoteId === id) setActiveNoteId(null);
};
const handleRestoreCarnet = (id: string) => {
setCarnets(carnets.map(c => c.id === id ? { ...c, isDeleted: false, deletedAt: undefined } : c));
// Optionally restore linked notes too? User might expect that.
setNotes(notes.map(n => n.carnetId === id ? { ...n, isDeleted: false, deletedAt: undefined } : n));
};
const handleRestoreNote = (id: string) => {
setNotes(notes.map(n => n.id === id ? { ...n, isDeleted: false, deletedAt: undefined } : n));
};
const handlePermanentDeleteNote = (id: string) => {
setNotes(notes.filter(n => n.id !== id));
};
const handlePermanentDeleteCarnet = (id: string) => {
const idsToDelete = new Set<string>([id]);
const addChildren = (parentId: string) => {
carnets.forEach(c => {
if (c.parentId === parentId) {
idsToDelete.add(c.id);
addChildren(c.id);
}
});
};
addChildren(id);
setCarnets(carnets.filter(c => !idsToDelete.has(c.id)));
setNotes(notes.filter(n => !idsToDelete.has(n.carnetId)));
};
const handleAddNote = (e: React.FormEvent) => {
e.preventDefault();
if (!newNoteTitle.trim() || !newNoteContent.trim()) return;
const newNote: Note = {
id: `n-${Date.now()}`,
carnetId: activeCarnetId,
title: newNoteTitle,
date: new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric' }).format(new Date()),
content: newNoteContent,
imageUrl: 'https://images.unsplash.com/photo-1487958449943-2429e8be8625?auto=format&fit=crop&q=80&w=800&h=600',
tags: []
};
setNotes([newNote, ...notes]);
setNewNoteTitle('');
setNewNoteContent('');
setShowNewNoteModal(false);
setActiveNoteId(newNote.id);
};
const handleConvertIdeaToNote = (idea: BrainstormIdea) => {
const newNote: Note = {
id: `n-gen-${Date.now()}`,
carnetId: activeCarnetId,
title: idea.title,
date: new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric' }).format(new Date()),
content: `${idea.description}\n\n---\n**Connection to seed:** ${idea.connectionToSeed}\n**Novelty Score:** ${idea.noveltyScore}/10`,
imageUrl: 'https://images.unsplash.com/photo-1497366216548-37526070297c?auto=format&fit=crop&q=80&w=800&h=600',
tags: [{ id: 't-ai', label: 'AI Generated', type: 'ai' }]
};
setNotes([newNote, ...notes]);
setActiveView('notebooks');
setActiveNoteId(newNote.id);
};
return (
<div className={`h-screen flex bg-paper transition-colors duration-500 overflow-hidden font-sans ${isDarkMode ? 'dark' : ''}`}>
<Sidebar
activeView={activeView}
isDarkMode={isDarkMode}
setIsDarkMode={setIsDarkMode}
setActiveView={setActiveView}
carnets={carnets}
notes={notes}
activeCarnetId={activeCarnetId}
activeNoteId={activeNoteId}
setActiveCarnetId={setActiveCarnetId}
setActiveNoteId={setActiveNoteId}
setShowNewCarnetModal={(show, parentId, isRenaming, carnetId) => {
setShowNewCarnetModal({ isOpen: show, parentId, isRenaming, carnetId });
if (isRenaming && carnetId) {
const carnet = carnets.find(c => c.id === carnetId);
if (carnet) setNewCarnetName(carnet.name);
} else {
setNewCarnetName('');
}
}}
onDeleteCarnet={handleDeleteCarnet}
onMoveCarnet={(draggedId, targetId) => {
if (draggedId === targetId) return;
// Basic circular check
const isDescendant = (parentId: string, potentialChildId: string): boolean => {
const childIds = carnets.filter(c => c.parentId === parentId).map(c => c.id);
if (childIds.includes(potentialChildId)) return true;
return childIds.some(id => isDescendant(id, potentialChildId));
};
if (targetId && isDescendant(draggedId, targetId)) {
console.warn("Cannot move a notebook inside its own descendant");
return;
}
setCarnets(prev => prev.map(c => c.id === draggedId ? { ...c, parentId: targetId } : c));
}}
/>
<main className="flex-1 relative overflow-hidden flex bg-paper dark:bg-dark-paper transition-colors duration-500">
<AnimatePresence mode="wait">
{(activeView === 'notebooks' || activeView === 'shared' || activeView === 'reminders') && (
<motion.div
key={activeView}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
className="h-full w-full"
>
<NotebooksView
activeNoteId={activeNoteId}
activeCarnet={activeCarnet}
filteredNotes={filteredNotes}
activeNote={activeNote}
setActiveNoteId={setActiveNoteId}
togglePin={togglePin}
setShowNewNoteModal={setShowNewNoteModal}
isAISidebarOpen={isAISidebarOpen}
setIsAISidebarOpen={setIsAISidebarOpen}
selectedTagIds={selectedTagIds}
setSelectedTagIds={setSelectedTagIds}
allNotes={notes}
activeCarnetId={activeCarnetId}
setShowNewCarnetModal={(show, parentId, isRenaming, carnetId) => setShowNewCarnetModal({ isOpen: show, parentId, isRenaming, carnetId })}
onDeleteNote={handleDeleteNote}
onBrainstormNote={handleBrainstormNote}
/>
</motion.div>
)}
{activeView === 'trash' && (
<motion.div
key="trash"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
className="h-full w-full"
>
<TrashView
deletedNotes={notes.filter(n => n.isDeleted)}
deletedCarnets={carnets.filter(c => c.isDeleted)}
onRestoreNote={handleRestoreNote}
onRestoreCarnet={handleRestoreCarnet}
onPermanentDeleteNote={handlePermanentDeleteNote}
onPermanentDeleteCarnet={handlePermanentDeleteCarnet}
onEmptyTrash={() => {
setNotes(notes.filter(n => !n.isDeleted));
setCarnets(carnets.filter(c => !c.isDeleted));
}}
/>
</motion.div>
)}
{activeView === 'agents' && (
<motion.div
key="agents"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
className="h-full w-full"
>
<AgentsView
selectedAgentId={selectedAgentId}
setSelectedAgentId={setSelectedAgentId}
carnets={carnets}
/>
</motion.div>
)}
{activeView === 'settings' && (
<motion.div
key="settings"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
className="h-full w-full"
>
<SettingsView
activeSettingsTab={activeSettingsTab}
setActiveSettingsTab={setActiveSettingsTab}
/>
</motion.div>
)}
{activeView === 'brainstorm' && (
<motion.div
key="brainstorm"
initial={{ opacity: 0, scale: 1.05 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.4, ease: [0.23, 1, 0.32, 1] }}
className="h-full w-full"
>
<BrainstormView
notes={notes}
onConvertNote={handleConvertIdeaToNote}
/>
</motion.div>
)}
{activeView === 'insights' && (
<motion.div
key="insights"
initial={{ opacity: 0, scale: 1.05 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.4, ease: [0.23, 1, 0.32, 1] }}
className="h-full w-full"
>
<InsightsView
notes={notes}
onUpdateNotes={setNotes}
onNoteSelect={(noteId) => {
setActiveView('notebooks');
setActiveNoteId(noteId);
}}
/>
</motion.div>
)}
{activeView === 'temporal' && (
<motion.div
key="temporal"
initial={{ opacity: 0, scale: 1.05 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.4, ease: [0.23, 1, 0.32, 1] }}
className="h-full w-full"
>
<TemporalView
notes={notes}
accessLogs={accessLogs}
onNoteSelect={(noteId) => {
setActiveView('notebooks');
setActiveNoteId(noteId);
}}
/>
</motion.div>
)}
</AnimatePresence>
<AISidebar
isOpen={isAISidebarOpen}
setIsOpen={setIsAISidebarOpen}
activeNote={activeNote}
aiTab={aiTab}
setAiTab={setAiTab}
selectedTone={selectedTone}
setSelectedTone={setSelectedTone}
carnets={carnets}
/>
</main>
{/* Modals */}
<AnimatePresence>
{showNewCarnetModal.isOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setShowNewCarnetModal({ isOpen: false })}
className="absolute inset-0 bg-ink/40 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="relative w-full max-w-md bg-paper dark:bg-dark-paper border border-border shadow-2xl rounded-2xl p-8"
>
<h3 className="text-2xl font-serif font-medium text-ink dark:text-dark-ink mb-2">
{showNewCarnetModal.isRenaming ? 'Rename Carnet' : (showNewCarnetModal.parentId ? 'Create Sub-Carnet' : 'Create New Carnet')}
</h3>
{showNewCarnetModal.parentId && !showNewCarnetModal.isRenaming && (
<p className="text-[10px] text-concrete uppercase tracking-widest font-bold mb-6">
Inside: {carnets.find(c => c.id === showNewCarnetModal.parentId)?.name}
</p>
)}
<form onSubmit={handleAddCarnet} className="space-y-6">
<div>
<label className="block text-[11px] uppercase tracking-widest font-bold text-muted-ink mb-2">Notebook Name</label>
<input
autoFocus
type="text"
value={newCarnetName}
onChange={(e) => setNewCarnetName(e.target.value)}
placeholder="E.g., Sustainable Patterns"
className="w-full bg-white dark:bg-[#2A2A2A] border border-border rounded-lg px-4 py-3 outline-none focus:border-ink transition-colors font-serif italic text-lg text-ink dark:text-dark-ink"
/>
</div>
<div className="flex gap-4 pt-4">
<button
type="button"
onClick={() => {
setShowNewCarnetModal({ isOpen: false });
setNewCarnetName('');
}}
className="flex-1 py-3 border border-border rounded-lg text-sm font-medium hover:bg-slate-50 dark:hover:bg-white/5 transition-colors text-ink dark:text-dark-ink"
>
Cancel
</button>
<button
type="submit"
className="flex-1 py-3 bg-ink dark:bg-ochre text-paper rounded-lg text-sm font-medium hover:opacity-90 transition-opacity"
>
{showNewCarnetModal.isRenaming ? 'Rename' : 'Create Notebook'}
</button>
</div>
</form>
</motion.div>
</div>
)}
{showNewNoteModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setShowNewNoteModal(false)}
className="absolute inset-0 bg-ink/40 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="relative w-full max-w-2xl bg-paper dark:bg-dark-paper border border-border shadow-2xl rounded-2xl p-10"
>
<AnimatePresence>
{slashMenu?.isOpen && (
<SlashMenu
position={{ top: slashMenu.top, left: slashMenu.left }}
onSelect={(type) => { console.log(type); setSlashMenu(null); }}
onClose={() => setSlashMenu(null)}
/>
)}
</AnimatePresence>
<h3 className="text-3xl font-serif font-medium text-ink dark:text-dark-ink mb-8">Add Architectural Note</h3>
<form onSubmit={handleAddNote} className="space-y-8">
<div>
<label className="block text-[11px] uppercase tracking-widest font-bold text-muted-ink mb-2">Concept Title</label>
<input
autoFocus
type="text"
value={newNoteTitle}
onChange={(e) => setNewNoteTitle(e.target.value)}
placeholder="Enter the title of your study..."
className="w-full bg-white dark:bg-[#2A2A2A] border border-border rounded-lg px-5 py-4 outline-none focus:border-ink transition-colors font-serif text-2xl text-ink dark:text-dark-ink"
/>
</div>
<div>
<label className="block text-[11px] uppercase tracking-widest font-bold text-muted-ink mb-2">Observations & Analysis</label>
<textarea
value={newNoteContent}
onChange={(e) => setNewNoteContent(e.target.value)}
onKeyDown={handleEditorKeyDown}
placeholder="Describe the spatial logic, materiality, and light interactions... (Type '/' for commands)"
rows={6}
className="w-full bg-white dark:bg-[#2A2A2A] border border-border rounded-lg px-5 py-4 outline-none focus:border-ink transition-colors font-light leading-relaxed resize-none text-ink dark:text-dark-ink"
/>
</div>
<div className="flex gap-4 pt-4">
<button
type="button"
onClick={() => setShowNewNoteModal(false)}
className="flex-1 py-4 border border-border rounded-lg text-sm font-medium hover:bg-slate-50 dark:hover:bg-white/5 transition-colors text-ink dark:text-dark-ink"
>
Cancel
</button>
<button
type="submit"
className="flex-1 py-4 bg-ink dark:bg-ochre text-paper rounded-lg text-sm font-medium hover:opacity-90 transition-opacity"
>
Save Note
</button>
</div>
</form>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
);
}