From 60a3fe54531a3aea86aa245bfb7004b54c9d1e0b Mon Sep 17 00:00:00 2001 From: Antigravity Date: Sat, 9 May 2026 12:58:16 +0000 Subject: [PATCH] UI Stabilization: Global color theme updates (#75B2D6), AI Assistant styling refactor, and navigation fixes --- .../general/general-settings-client.tsx | 24 + memento-note/app/(main)/settings/layout.tsx | 2 +- memento-note/app/actions/ai-settings.ts | 5 + memento-note/app/actions/notes.ts | 178 +- .../app/api/agents/run-for-note/route.ts | 20 +- .../app/api/ai/enrich-from-resource/route.ts | 9 +- memento-note/app/api/chat/route.ts | 362 +--- memento-note/app/globals.css | 567 ++++-- memento-note/components/ai-chat.tsx | 34 +- .../components/batch-organization-dialog.tsx | 2 +- .../components/contextual-ai-chat.tsx | 1551 +++++++---------- .../components/contextual-ai-chat.tsx.bak | 1049 +++++++++++ memento-note/components/home-client.tsx | 38 +- memento-note/components/lab/canvas-board.tsx | 21 +- memento-note/components/label-badge.tsx | 22 +- memento-note/components/markdown-content.tsx | 3 +- .../components/note-document-info-panel.tsx | 149 +- .../note-editor/note-editor-full-page.tsx | 4 +- .../note-editor/note-editor-toolbar.tsx | 26 +- .../note-editor/note-share-dialog.tsx | 222 +++ .../note-editor/note-title-block.tsx | 36 +- .../components/note-history-modal.tsx | 56 +- .../components/note-inline-editor.tsx | 76 +- memento-note/components/note-input.tsx | 11 +- .../components/notification-panel.tsx | 345 ++-- memento-note/components/rich-text-editor.tsx | 127 +- .../components/settings/SettingsNav.tsx | 6 +- memento-note/components/sidebar.tsx | 120 +- memento-note/components/ui/alert-dialog.tsx | 2 +- memento-note/components/ui/avatar.tsx | 2 +- memento-note/components/ui/dialog.tsx | 2 +- memento-note/components/ui/toast.tsx | 22 +- .../8c3fef96-1690-45dc-a8e9-80ad9ed337c3.png | Bin 0 -> 45485 bytes .../b615a92f-b1be-4d85-aeb7-7c0b96c6fb09.png | Bin 0 -> 157678 bytes .../d2e6ebcd-c3c0-4ef6-91f3-4425e98bfd53.png | Bin 0 -> 200494 bytes .../e9b9a04e-d8ea-4677-b7f4-edc51150bc64.png | Bin 0 -> 212377 bytes .../lib/ai/services/agent-executor.service.ts | 44 +- .../services/auto-label-creation.service.ts | 3 +- memento-note/lib/ai/tools/pptx.tool.ts | 5 +- memento-note/lib/ai/tools/slides.tool.ts | 50 +- memento-note/lib/markdown-to-html.ts | 222 +-- memento-note/lib/types.ts | 1 + memento-note/locales/en.json | 71 +- memento-note/locales/fr.json | 91 +- memento-note/package-lock.json | 146 +- memento-note/package.json | 6 + memento-note/prisma/schema.prisma | 2 + 47 files changed, 3585 insertions(+), 2149 deletions(-) create mode 100644 memento-note/components/contextual-ai-chat.tsx.bak create mode 100644 memento-note/components/note-editor/note-share-dialog.tsx create mode 100644 memento-note/data/uploads/notes/8c3fef96-1690-45dc-a8e9-80ad9ed337c3.png create mode 100644 memento-note/data/uploads/notes/b615a92f-b1be-4d85-aeb7-7c0b96c6fb09.png create mode 100644 memento-note/data/uploads/notes/d2e6ebcd-c3c0-4ef6-91f3-4425e98bfd53.png create mode 100644 memento-note/data/uploads/notes/e9b9a04e-d8ea-4677-b7f4-edc51150bc64.png diff --git a/memento-note/app/(main)/settings/general/general-settings-client.tsx b/memento-note/app/(main)/settings/general/general-settings-client.tsx index a0c6b6e..1f1d2e1 100644 --- a/memento-note/app/(main)/settings/general/general-settings-client.tsx +++ b/memento-note/app/(main)/settings/general/general-settings-client.tsx @@ -12,6 +12,7 @@ interface GeneralSettingsClientProps { preferredLanguage: string emailNotifications: boolean desktopNotifications: boolean + autoSave: boolean } } @@ -21,6 +22,7 @@ export function GeneralSettingsClient({ initialSettings }: GeneralSettingsClient const [language, setLanguage] = useState(initialSettings.preferredLanguage || 'auto') const [emailNotifications, setEmailNotifications] = useState(initialSettings.emailNotifications ?? false) const [desktopNotifications, setDesktopNotifications] = useState(initialSettings.desktopNotifications ?? false) + const [autoSave, setAutoSave] = useState(initialSettings.autoSave ?? true) const handleLanguageChange = async (value: string) => { setLanguage(value) @@ -47,6 +49,12 @@ export function GeneralSettingsClient({ initialSettings }: GeneralSettingsClient await updateAISettings({ desktopNotifications: enabled }) toast.success(t('settings.settingsSaved') || 'Saved') } + + const handleAutoSaveChange = async (enabled: boolean) => { + setAutoSave(enabled) + await updateAISettings({ autoSave: enabled }) + toast.success(t('settings.settingsSaved') || 'Saved') + } return (
@@ -140,6 +148,22 @@ export function GeneralSettingsClient({ initialSettings }: GeneralSettingsClient
+ +
+
+

{t('settings.autoSave') || 'Auto-Save'}

+

{t('settings.autoSaveDesc') || 'Sauvegarder automatiquement les modifications'}

+
+ +
diff --git a/memento-note/app/(main)/settings/layout.tsx b/memento-note/app/(main)/settings/layout.tsx index 30894bb..59624c7 100644 --- a/memento-note/app/(main)/settings/layout.tsx +++ b/memento-note/app/(main)/settings/layout.tsx @@ -8,7 +8,7 @@ export default function SettingsLayout({ children: React.ReactNode }) { return ( -
+
{/* Architectural header — matches Agents page */}
diff --git a/memento-note/app/actions/ai-settings.ts b/memento-note/app/actions/ai-settings.ts index 61c5391..3b1e6e2 100644 --- a/memento-note/app/actions/ai-settings.ts +++ b/memento-note/app/actions/ai-settings.ts @@ -24,6 +24,7 @@ export type UserAISettingsData = { noteHistory?: boolean noteHistoryMode?: 'manual' | 'auto' fontFamily?: 'inter' | 'playfair' | 'jetbrains' | 'system' + autoSave?: boolean } /** Only fields that exist on `UserAISettings` in Prisma (excludes e.g. `theme`, which lives on `User`). */ @@ -47,6 +48,7 @@ const USER_AI_SETTINGS_PRISMA_KEYS = [ 'noteHistory', 'noteHistoryMode', 'fontFamily', + 'autoSave', ] as const type UserAISettingsPrismaKey = (typeof USER_AI_SETTINGS_PRISMA_KEYS)[number] @@ -158,6 +160,7 @@ const getCachedAISettings = unstable_cache( noteHistory: false, noteHistoryMode: 'manual' as const, fontFamily: 'inter' as const, + autoSave: true, } } @@ -192,6 +195,7 @@ const getCachedAISettings = unstable_cache( noteHistory: settings.noteHistory ?? false, noteHistoryMode: (settings.noteHistoryMode ?? 'manual') as 'manual' | 'auto', fontFamily: (settings.fontFamily || 'inter') as 'inter' | 'playfair' | 'jetbrains' | 'system', + autoSave: settings.autoSave ?? true, } } catch (error) { console.error('Error getting AI settings:', error) @@ -217,6 +221,7 @@ const getCachedAISettings = unstable_cache( noteHistory: false, noteHistoryMode: 'manual' as const, fontFamily: 'inter' as const, + autoSave: true, } } }, diff --git a/memento-note/app/actions/notes.ts b/memento-note/app/actions/notes.ts index 5fbafca..36cd9c8 100644 --- a/memento-note/app/actions/notes.ts +++ b/memento-note/app/actions/notes.ts @@ -214,7 +214,7 @@ async function syncNoteLabels(noteId: string, labelNames: string[], notebookId: if (Array.isArray(parsed)) { parsed.filter((x: any) => typeof x === 'string').forEach((x: string) => namesInUse.add(x.toLowerCase())) } - } catch {} + } catch { } } } // Delete labels not in use @@ -680,91 +680,91 @@ export async function createNote(data: { const notebookId = data.notebookId const hasUserLabels = data.labels && data.labels.length > 0 - // Use setImmediate-like pattern to not block the response - ;(async () => { - try { - // Background task 1: Generate embedding - const bgConfig = await getSystemConfig() - const provider = getAIProvider(bgConfig) - const embedding = await provider.getEmbeddings(content) - if (embedding) { - await prisma.noteEmbedding.upsert({ - where: { noteId: noteId }, - create: { noteId: noteId, embedding: JSON.stringify(embedding) }, - update: { embedding: JSON.stringify(embedding) } - }) - } - } catch (e) { - console.error('[BG] Embedding generation failed:', e) - } - - // Background task 2: Auto-labeling (only if no user labels and has notebook) - if (!hasUserLabels && notebookId) { + // Use setImmediate-like pattern to not block the response + ; (async () => { try { - const userAISettings = await getAISettings(userId) - const autoLabelingEnabled = userAISettings.autoLabeling !== false - const autoLabelingConfidence = await getConfigNumber('AUTO_LABELING_CONFIDENCE_THRESHOLD', 70) + // Background task 1: Generate embedding + const bgConfig = await getSystemConfig() + const provider = getAIProvider(bgConfig) + const embedding = await provider.getEmbeddings(content) + if (embedding) { + await prisma.noteEmbedding.upsert({ + where: { noteId: noteId }, + create: { noteId: noteId, embedding: JSON.stringify(embedding) }, + update: { embedding: JSON.stringify(embedding) } + }) + } + } catch (e) { + console.error('[BG] Embedding generation failed:', e) + } - console.log('[BG] Auto-labeling check: enabled=', autoLabelingEnabled, 'confidence=', autoLabelingConfidence, 'notebookId=', notebookId) + // Background task 2: Auto-labeling (only if no user labels and has notebook) + if (!hasUserLabels && notebookId) { + try { + const userAISettings = await getAISettings(userId) + const autoLabelingEnabled = userAISettings.autoLabeling !== false + const autoLabelingConfidence = await getConfigNumber('AUTO_LABELING_CONFIDENCE_THRESHOLD', 70) - if (autoLabelingEnabled) { - // Detect user's language from their existing notes for localized prompts - let userLang = 'en' - try { - const langResult = await prisma.note.groupBy({ - by: ['language'], - where: { userId, language: { not: null } }, - _count: true, - orderBy: { _count: { language: 'desc' } }, - take: 1, - }) - if (langResult.length > 0 && langResult[0].language) { - userLang = langResult[0].language - } - } catch {} + console.log('[BG] Auto-labeling check: enabled=', autoLabelingEnabled, 'confidence=', autoLabelingConfidence, 'notebookId=', notebookId) - const suggestions = await contextualAutoTagService.suggestLabels( - content, - notebookId, - userId, - userLang - ) + if (autoLabelingEnabled) { + // Detect user's language from their existing notes for localized prompts + let userLang = 'en' + try { + const langResult = await prisma.note.groupBy({ + by: ['language'], + where: { userId, language: { not: null } }, + _count: true, + orderBy: { _count: { language: 'desc' } }, + take: 1, + }) + if (langResult.length > 0 && langResult[0].language) { + userLang = langResult[0].language + } + } catch { } - console.log('[BG] Auto-labeling suggestions:', suggestions.length, suggestions.map(s => s.label)) + const suggestions = await contextualAutoTagService.suggestLabels( + content, + notebookId, + userId, + userLang + ) - const appliedLabels = suggestions - .filter(s => s.confidence >= autoLabelingConfidence) - .map(s => s.label) + console.log('[BG] Auto-labeling suggestions:', suggestions.length, suggestions.map(s => s.label)) - if (appliedLabels.length > 0) { - // Merge with existing labels - const existing = await prisma.note.findUnique({ - where: { id: noteId }, - select: { labels: true }, - }) - let existingNames: string[] = [] - if (existing?.labels) { - try { - const parsed = existing.labels as unknown - existingNames = Array.isArray(parsed) - ? parsed.filter((n): n is string => typeof n === 'string' && n.trim().length > 0) - : [] - } catch { existingNames = [] } - } - const merged = [...new Set([...existingNames, ...appliedLabels])] - await syncNoteLabels(noteId, merged, notebookId ?? null, userId) - if (!data.skipRevalidation) { - revalidatePath('/') + const appliedLabels = suggestions + .filter(s => s.confidence >= autoLabelingConfidence) + .map(s => s.label) + + if (appliedLabels.length > 0) { + // Merge with existing labels + const existing = await prisma.note.findUnique({ + where: { id: noteId }, + select: { labels: true }, + }) + let existingNames: string[] = [] + if (existing?.labels) { + try { + const parsed = existing.labels as unknown + existingNames = Array.isArray(parsed) + ? parsed.filter((n): n is string => typeof n === 'string' && n.trim().length > 0) + : [] + } catch { existingNames = [] } + } + const merged = [...new Set([...existingNames, ...appliedLabels])] + await syncNoteLabels(noteId, merged, notebookId ?? null, userId) + if (!data.skipRevalidation) { + revalidatePath('/') + } } } + } catch (error) { + console.error('[BG] Auto-labeling failed:', error) } - } catch (error) { - console.error('[BG] Auto-labeling failed:', error) + } else { + console.log('[BG] Auto-labeling skipped: hasUserLabels=', hasUserLabels, 'notebookId=', notebookId) } - } else { - console.log('[BG] Auto-labeling skipped: hasUserLabels=', hasUserLabels, 'notebookId=', notebookId) - } - })() + })() return parseNote(note) } catch (error) { @@ -819,21 +819,21 @@ export async function updateNote(id: string, data: { if (data.content !== undefined) { const noteId = id const content = data.content - ;(async () => { - try { - const provider = getAIProvider(await getSystemConfig()); - const embedding = await provider.getEmbeddings(content); - if (embedding) { - await prisma.noteEmbedding.upsert({ - where: { noteId: noteId }, - create: { noteId: noteId, embedding: JSON.stringify(embedding) }, - update: { embedding: JSON.stringify(embedding) } - }) + ; (async () => { + try { + const provider = getAIProvider(await getSystemConfig()); + const embedding = await provider.getEmbeddings(content); + if (embedding) { + await prisma.noteEmbedding.upsert({ + where: { noteId: noteId }, + create: { noteId: noteId, embedding: JSON.stringify(embedding) }, + update: { embedding: JSON.stringify(embedding) } + }) + } + } catch (e) { + console.error('[BG] Embedding regeneration failed:', e); } - } catch (e) { - console.error('[BG] Embedding regeneration failed:', e); - } - })() + })() } if ('checkItems' in data) updateData.checkItems = data.checkItems ? JSON.stringify(data.checkItems) : null diff --git a/memento-note/app/api/agents/run-for-note/route.ts b/memento-note/app/api/agents/run-for-note/route.ts index 1272314..aab805a 100644 --- a/memento-note/app/api/agents/run-for-note/route.ts +++ b/memento-note/app/api/agents/run-for-note/route.ts @@ -30,11 +30,12 @@ export async function POST(req: NextRequest) { const userId = session.user.id const body = await req.json() - const { noteId, type, theme, style } = body as { + const { noteId, type, theme, style, language } = body as { noteId: string type: GenerateType theme?: string style?: string + language?: string } if (!noteId || !type || !TYPE_DEFAULTS[type]) { @@ -50,15 +51,26 @@ export async function POST(req: NextRequest) { } const defaults = TYPE_DEFAULTS[type] + const isEn = language === 'English' + + let role = defaults.role + if (isEn) { + if (type === 'slide-generator') { + role = 'Create a professional and visual PowerPoint presentation from the provided note content.' + } else { + role = 'Generate a clear and professional Excalidraw diagram from the provided note content.' + } + } + const agentName = type === 'slide-generator' - ? `Slides — ${(note.title || 'Note').substring(0, 40)}` - : `Diagramme — ${(note.title || 'Note').substring(0, 40)}` + ? `${isEn ? 'Slides' : 'Présentation'} — ${(note.title || 'Note').substring(0, 40)}` + : `${isEn ? 'Diagram' : 'Diagramme'} — ${(note.title || 'Note').substring(0, 40)}` const agent = await prisma.agent.create({ data: { name: agentName, type, - role: defaults.role, + role, tools: JSON.stringify(defaults.tools), maxSteps: defaults.maxSteps, frequency: 'one-shot', diff --git a/memento-note/app/api/ai/enrich-from-resource/route.ts b/memento-note/app/api/ai/enrich-from-resource/route.ts index 0ce145b..fabe597 100644 --- a/memento-note/app/api/ai/enrich-from-resource/route.ts +++ b/memento-note/app/api/ai/enrich-from-resource/route.ts @@ -10,7 +10,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } - const { existingContent, resourceText, mode, language } = await request.json() + const { existingContent, resourceText, mode, language, format } = await request.json() if (!resourceText || typeof resourceText !== 'string') { return NextResponse.json({ error: 'resourceText is required' }, { status: 400 }) @@ -20,6 +20,7 @@ export async function POST(request: NextRequest) { } const lang = language || 'fr' + const outputFormat = format === 'html' ? 'HTML (with proper tags like

,

,

    ,
  • )' : 'Markdown (with ##, -, **, etc.)' const config = await getSystemConfig() const provider = getTagsProvider(config) @@ -30,6 +31,7 @@ export async function POST(request: NextRequest) { prompt = `You are an expert note editor. Your task is to enrich an existing note by adding relevant information from a provided resource, WITHOUT modifying or rewriting the existing content. LANGUAGE RULE: Respond in ${lang}. Match the language of the existing note. +FORMAT RULE: Respond in ${outputFormat}. EXISTING NOTE: --- @@ -46,13 +48,14 @@ INSTRUCTIONS: - Append ONLY new, non-redundant information from the resource below the existing content - Use a clear separator (e.g., "---" or a new section heading) between existing and new content - Skip information already covered in the existing note -- Format the new content consistently with the existing note style +- Format the new content consistently with the existing note style and the requested FORMAT RULE - Respond ONLY with the enriched note content, no explanations` } else { // Merge: intelligently rewrite integrating both sources prompt = `You are an expert note writer. Your task is to intelligently merge an existing note with a resource into a single, coherent, well-structured document. LANGUAGE RULE: Respond in ${lang}. Match the language of the existing note. +FORMAT RULE: Respond in ${outputFormat}. EXISTING NOTE: --- @@ -69,7 +72,7 @@ INSTRUCTIONS: - Eliminate redundancy — include each piece of information only once - Preserve the key ideas from both sources - Maintain a logical structure with clear headings if appropriate -- Keep the tone and style consistent +- Keep the tone and style consistent with the requested FORMAT RULE - Respond ONLY with the merged content, no meta-commentary or explanations` } diff --git a/memento-note/app/api/chat/route.ts b/memento-note/app/api/chat/route.ts index 00dd784..a24cd0a 100644 --- a/memento-note/app/api/chat/route.ts +++ b/memento-note/app/api/chat/route.ts @@ -6,7 +6,6 @@ import { prisma } from '@/lib/prisma' import { auth } from '@/auth' import { loadTranslations, getTranslationValue, SupportedLanguage } from '@/lib/i18n' import { toolRegistry } from '@/lib/ai/tools' -import { stepCountIs } from 'ai' import { readFile } from 'fs/promises' import path from 'path' @@ -47,36 +46,32 @@ export async function POST(req: Request) { } const userId = session.user.id - // 2. Parse request body — messages arrive as UIMessage[] from DefaultChatTransport + // 2. Parse request body const body = await req.json() - - const { messages: rawMessages, conversationId, notebookId, language, webSearch, noteContext } = body as { + const { messages: rawMessages, conversationId, notebookId, language, webSearch, noteContext, format } = body as { messages: UIMessage[] conversationId?: string notebookId?: string language?: string webSearch?: boolean noteContext?: { title: string; content: string; tone: string; images?: string[] } + format?: 'html' | 'markdown' } - // Convert UIMessages to CoreMessages for streamText const incomingMessages = toCoreMessages(rawMessages) - // 3. Manage conversation (create or fetch) + // 3. Manage conversation let conversation: { id: string; messages: Array<{ role: string; content: string }> } - if (conversationId) { const existing = await prisma.conversation.findUnique({ where: { id: conversationId, userId }, include: { messages: { orderBy: { createdAt: 'asc' } } }, }) - if (!existing) { - return new Response('Conversation not found', { status: 404 }) - } + if (!existing) return new Response('Conversation not found', { status: 404 }) conversation = existing } else { const userMessage = incomingMessages[incomingMessages.length - 1]?.content || 'New conversation' - const created = await prisma.conversation.create({ + conversation = await prisma.conversation.create({ data: { userId, notebookId: notebookId || null, @@ -84,33 +79,21 @@ export async function POST(req: Request) { }, include: { messages: true }, }) - conversation = created } // 4. RAG retrieval const currentMessage = incomingMessages[incomingMessages.length - 1]?.content || '' - - // Load translations for the requested language const lang = (language || 'en') as SupportedLanguage const translations = await loadTranslations(lang) const untitledText = getTranslationValue(translations, 'notes.untitled') || 'Untitled' - // If a notebook is selected, fetch its recent notes directly as context - // This ensures the AI always has access to the notebook content, - // even for vague queries like "what's in this notebook?" let notebookContext = '' let searchNotes = '' - // When scope is "this note" (noteContext present), skip RAG retrieval entirely - // The note content is already injected as copilotContext below if (!noteContext) { if (notebookId) { const notebookNotes = await prisma.note.findMany({ - where: { - notebookId, - userId, - trashedAt: null, - }, + where: { notebookId, userId, trashedAt: null }, orderBy: { updatedAt: 'desc' }, take: 20, select: { id: true, title: true, content: true, updatedAt: true }, @@ -122,7 +105,6 @@ export async function POST(req: Request) { } } - // Also run semantic search for the specific query let searchResults: any[] = [] try { searchResults = await semanticSearchService.search(currentMessage, { @@ -131,21 +113,16 @@ export async function POST(req: Request) { threshold: notebookId ? 0.3 : 0.5, defaultTitle: untitledText, }) - } catch { - // Search failure should not block chat - } + } catch {} searchNotes = searchResults .map((r) => `NOTE [${r.title || untitledText}]: ${r.content}`) .join('\n\n---\n\n') } - // Combine: full notebook context + semantic search results (deduplicated) const contextNotes = [notebookContext, searchNotes].filter(Boolean).join('\n\n---\n\n') - // 5. System prompt synthesis with RAG context - // Language-aware prompts to avoid forcing French responses - // Note: lang is already declared above when loading translations + // 5. System prompt synthesis const promptLang: Record = { en: { contextWithNotes: `## User's notes\n\n${contextNotes}\n\nWhen using info from the notes above, cite the source note title in parentheses, e.g.: "Deployment is done via Docker (💻 Development Guide)". Don't copy word for word — rephrase. If the notes don't cover the topic, say so and supplement with your general knowledge.`, @@ -153,14 +130,24 @@ export async function POST(req: Request) { system: `You are the AI assistant of Memento. The user asks you questions about their projects, technical docs, and notes. You must respond in a structured and helpful way. ## Format rules -- Use markdown freely: headings (##, ###), lists, code blocks, bold, tables — anything that makes the response readable. +- ${format === 'html' ? `Respond MANDATORILY using valid HTML fragments (e.g.,

    , , ,

      ,
    • ,

      , , ,
      ). +- Do NOT use Markdown symbols (no #, *, -, etc.). +- Do not wrap your HTML code in a Markdown code block.` : 'Use markdown freely: headings (##, ###), lists, code blocks, bold, tables — anything that makes the response readable.'} - Structure your response with sections for technical questions or complex topics. -- For simple, short questions, a direct paragraph is enough. +- For simple, short questions, a direct paragraph is enough.` + (format === 'html' ? ` + +## HTML OUTPUT EXAMPLE +

      Section Title

      +

      Here is an explanation with bold text and a list:

      +
        +
      • First important point
      • +
      • Second important point
      • +
      ` : '') + ` ## Tone rules - Natural tone, neither corporate nor too casual. -- No unnecessary intro phrases ("Here's what I found", "Based on your notes"). Answer directly. -- No upsell questions at the end ("Would you like me to...", "Do you want..."). If you have useful additional info, just give it. +- No unnecessary intro phrases. Answer directly. +- No upsell questions at the end. If you have useful additional info, just give it. - If the user says "Momento" they mean Momento (this app). ## About Momento @@ -170,171 +157,90 @@ Momento is an intelligent note-taking application. Key features include: - **Search**: Advanced semantic search to find notes by meaning, not just keywords, and Web Search integration. - **Agents**: Create specialized AI Agents with custom system prompts for specific recurring tasks. - **Lab**: Experimental AI tools for data analysis and deeper insights. -If the user asks how to use this tool, explain these features simply and helpfully. ## Available tools -You have access to these tools for deeper research: -- **note_search**: Search the user's notes by keyword or meaning. Use when the initial context above is insufficient or when the user asks about specific content in their notes. If a notebook is selected, pass its ID to restrict results. -- **note_read**: Read a specific note by ID. Use when note_search returns a note you need the full content of. -- **web_search**: Search the web for information. Use when the user asks about something not in their notes. -- **web_scrape**: Scrape a web page and return its content as markdown. Use when web_search returns a URL you need to read. - -## Tool usage rules -- You already have context from the user's notes above. Only use tools if you need more specific or additional information. -- Never invent note IDs, URLs, or notebook IDs. Use the IDs provided in the context or from tool results. -- For simple conversational questions (greetings, opinions, general knowledge), answer directly without using any tools.`, +You have access to: note_search, note_read, web_search, web_scrape. +Only use tools if you need more information. Never invent note IDs or URLs.`, }, fr: { - contextWithNotes: `## Notes de l'utilisateur\n\n${contextNotes}\n\nQuand tu utilises une info venant des notes ci-dessus, cite le titre de la note source entre parenthèses, ex: "Le déploiement se fait via Docker (💻 Development Guide)". Ne recopie pas mot pour mot — reformule. Si les notes ne couvrent pas le sujet, dis-le et complète avec tes connaissances générales.`, + contextWithNotes: `## Notes de l'utilisateur\n\n${contextNotes}\n\nQuand tu utilises une info venant des notes ci-dessus, cite le titre de la note source entre parenthèses, ex: "Le déploiement se fait via Docker (💻 Development Guide)". Ne recopie pas mot pour mot — reformule.`, contextNoNotes: "Aucune note pertinente trouvée pour cette question. Réponds avec tes connaissances générales.", system: `Tu es l'assistant IA de Memento. L'utilisateur te pose des questions sur ses projets, sa doc technique, ses notes. Tu dois répondre de façon structurée et utile. ## Règles de format -- Utilise le markdown librement : titres (##, ###), listes, code blocks, gras, tables — tout ce qui rend la réponse lisible. +- ${format === 'html' ? `Réponds OBLIGATOIREMENT en utilisant des fragments HTML valides (ex:

      , , ,

        ,
      • ,

        , , ,
        ). +- N'utilise PAS de symboles Markdown. +- Ne mets pas ton code HTML dans un bloc de code Markdown.` : '- Utilise le markdown librement : titres (##, ###), listes, code blocks, gras, tables.'} - Structure ta réponse avec des sections quand c'est une question technique ou un sujet complexe. -- Pour les questions simples et courtes, un paragraphe direct suffit. +- Pour les questions simples et courtes, un paragraphe direct suffit.` + (format === 'html' ? ` + +## EXEMPLE DE SORTIE HTML +

        Titre de section

        +

        Voici une explication avec du texte en gras et une liste :

        +
          +
        • Premier point important
        • +
        • Deuxième point important
        • +
        ` : '') + ` ## Règles de ton -- Ton naturel, ni corporate ni trop familier. -- Pas de phrase d'intro inutile ("Voici ce que j'ai trouvé", "Basé sur vos notes"). Réponds directement. -- Pas de question upsell à la fin ("Souhaitez-vous que je...", "Acceptez-vous que..."). Si tu as une info complémentaire utile, donne-la. +- Ton naturel, direct, sans phrases d'intro inutiles. +- Pas de question upsell à la fin. - Si l'utilisateur dit "Momento" il parle de Momento (cette application). ## À propos de Momento -Momento est une application de prise de notes intelligente. Ses fonctionnalités principales : -- **Éditeur de notes** : Prise de notes en Markdown riche avec un Copilot IA intégré pour réécrire, résumer ou traduire du texte. -- **Organisation** : Regroupement des notes dans des Carnets (Notebooks) et utilisation d'Étiquettes (Labels). -- **Recherche** : Recherche sémantique avancée pour trouver des notes par le sens, et recherche Web intégrée. -- **Agents** : Création d'Agents IA spécialisés avec des instructions personnalisées pour des tâches récurrentes. -- **Lab** : Outils IA expérimentaux pour l'analyse de données et les insights. -Si l'utilisateur demande comment utiliser cet outil, explique ces fonctionnalités simplement et avec bienveillance. +Momento est une application de prise de notes intelligente. Ses fonctionnalités : Éditeur Markdown riche, Copilot IA, Organisation par Carnets, Recherche sémantique, Agents IA, Lab. ## Outils disponibles -Tu as accès à ces outils pour des recherches approfondies : -- **note_search** : Cherche dans les notes de l'utilisateur par mot-clé ou sens. Utilise quand le contexte initial ci-dessus est insuffisant ou quand l'utilisateur demande du contenu spécifique dans ses notes. Si un carnet est sélectionné, passe son ID pour restreindre les résultats. -- **note_read** : Lit une note spécifique par son ID. Utilise quand note_search retourne une note dont tu as besoin du contenu complet. -- **web_search** : Recherche sur le web. Utilise quand l'utilisateur demande quelque chose qui n'est pas dans ses notes. -- **web_scrape** : Scrape une page web et retourne son contenu en markdown. Utilise quand web_search retourne une URL que tu veux lire. - -## Règles d'utilisation des outils -- Tu as déjà du contexte des notes de l'utilisateur ci-dessus. Utilise les outils seulement si tu as besoin d'informations plus spécifiques. -- N'invente jamais d'IDs de notes, d'URLs ou d'IDs de carnet. Utilise les IDs fournis dans le contexte ou les résultats d'outils. -- Pour les questions conversationnelles simples (salutations, opinions, connaissances générales), réponds directement sans utiliser d'outils.`, +Tu as accès à : note_search, note_read, web_search, web_scrape.`, }, fa: { - contextWithNotes: `## یادداشت‌های کاربر\n\n${contextNotes}\n\nهنگام استفاده از اطلاعات یادداشت‌های بالا، عنوان یادداشت منبع را در پرانتز ذکر کنید. کپی نکنید — بازنویسی کنید. اگر یادداشت‌ها موضوع را پوشش نمی‌دهند، بگویید و با دانش عمومی خود تکمیل کنید.`, + contextWithNotes: `## یادداشت‌های کاربر\n\n${contextNotes}\n\nهنگام استفاده از اطلاعات یادداشت‌های بالا، عنوان یادداشت منبع را در پرانتز ذکر کنید.`, contextNoNotes: "هیچ یادداشت مرتبطی برای این سؤال یافت نشد. با دانش عمومی خود پاسخ دهید.", system: `شما دستیار هوش مصنوعی Memento هستید. کاربر از شما درباره پروژه‌ها، مستندات فنی و یادداشت‌هایش سؤال می‌کند. باید به شکلی ساختاریافته و مفید پاسخ دهید. ## قوانین قالب‌بندی -- از مارک‌داون آزادانه استفاده کنید: عناوین (##, ###)، لیست‌ها، بلوک‌های کد، پررنگ، جداول. +- ${format === 'html' ? `حتماً از تگ‌های HTML معتبر استفاده کنید (مانند

        , , ,

          ,
        • ,

          ). +- از نمادهای مارک‌داون استفاده نکنید.` : 'از مارک‌داون آزادانه استفاده کنید: عناوین (##, ###)، لیست‌ها، بلوک‌های کد، پررنگ، جداول.'} - برای سؤالات فنی یا موضوعات پیچیده، پاسخ خود را بخش‌بندی کنید. -- برای سؤالات ساده و کوتاه، یک پاراگراف مستقیم کافی است. +- برای سؤالات ساده و کوتاه، یک پاراگراف مستقیم کافی است.` + (format === 'html' ? ` + +## نمونه خروجی HTML +

          عنوان بخش

          +

          این یک توضیح با متن برجسته و یک لیست است:

          +
            +
          • نکته اول
          • +
          • نکته دوم
          • +
          ` : '') + ` ## قوانین لحن -- لحن طبیعی، نه رسمی بیش از حد و نه خیلی غیررسمی. -- بدون جمله مقدمه اضافی. مستقیم پاسخ دهید. -- بدون سؤال فروشی در انتها. اگر اطلاعات تکمیلی مفید دارید، مستقیم بدهید. -- اگر کاربر "Momento" می‌گوید، منظورش Memento (این برنامه) است. - -## ابزارهای موجود -- **note_search**: جستجو در یادداشت‌های کاربر با کلیدواژه یا معنی. زمانی استفاده کنید که زمینه اولیه کافی نباشد. اگر دفترچه انتخاب شده، شناسه آن را ارسال کنید. -- **note_read**: خواندن یک یادداشت خاص با شناسه. زمانی استفاده کنید که note_search یادداشتی برگرداند که محتوای کامل آن را نیاز دارید. -- **web_search**: جستجو در وب. زمانی استفاده کنید که کاربر درباره چیزی خارج از یادداشت‌هایش می‌پرسد. -- **web_scrape**: استخراج محتوای صفحه وب. زمانی استفاده کنید که web_search نشانی‌ای برگرداند که می‌خواهید بخوانید. - -## قوانین استفاده از ابزارها -- شما از قبل زمینه‌ای از یادداشت‌های کاربر دارید. فقط در صورت نیاز به اطلاعات بیشتر از ابزارها استفاده کنید. -- هرگز شناسه یادداشت، نشانی یا شناسه دفترچه نسازید. از شناسه‌های موجود در زمینه یا نتایج ابزار استفاده کنید. -- برای سؤالات مکالمه‌ای ساده (سلام، نظرات، دانش عمومی)، مستقیم پاسخ دهید.`, +- لحن طبیعی، مستقیم، بدون مقدمه اضافی. +- اگر کاربر "Momento" می‌گوید، منظورش Memento (این برنامه) است.`, }, es: { - contextWithNotes: `## Notas del usuario\n\n${contextNotes}\n\nCuando uses información de las notas anteriores, cita el título de la nota fuente entre paréntesis. No copies palabra por palabra — reformula. Si las notas no cubren el tema, dilo y complementa con tu conocimiento general.`, + contextWithNotes: `## Notas del usuario\n\n${contextNotes}\n\nCuando uses información de las notas anteriores, cita el título de la nota fuente entre paréntesis.`, contextNoNotes: "No se encontraron notas relevantes para esta pregunta. Responde con tu conocimiento general.", - system: `Eres el asistente de IA de Memento. El usuario te hace preguntas sobre sus proyectos, documentación técnica y notas. Debes responder de forma estructurada y útil. + system: `Eres el asistente de IA de Memento. El usuario te hace preguntas sobre sus proyectos, documentación técnica y notas. ## Reglas de formato -- Usa markdown libremente: títulos (##, ###), listas, bloques de código, negritas, tablas. -- Estructura tu respuesta con secciones para preguntas técnicas o temas complejos. -- Para preguntas simples y cortas, un párrafo directo es suficiente. +- ${format === 'html' ? `Responde OBLIGATORIAMENTE usando fragmentos HTML válidos (ej:

          , , ,

            ,
          • ,

            , , ,
            ). +- NO uses símbolos Markdown.` : 'Usa markdown libremente: títulos (##, ###), listas, negritas, tablas.'} +- Estructura tu respuesta con secciones para temas complejos. +- Para preguntas simples, un párrafo directo es suficiente.` + (format === 'html' ? ` -## Reglas de tono -- Tono natural, ni corporativo ni demasiado informal. -- Sin frases de introducción innecesarias. Responde directamente. -- Sin preguntas de venta al final. Si tienes información complementaria útil, dala directamente. - -## Herramientas disponibles -- **note_search**: Busca en las notas del usuario por palabra clave o significado. Úsalo cuando el contexto inicial sea insuficiente. Si hay una libreta seleccionada, pasa su ID para restringir los resultados. -- **note_read**: Lee una nota específica por su ID. Úsalo cuando note_search devuelva una nota cuyo contenido completo necesites. -- **web_search**: Busca en la web. Úsalo cuando el usuario pregunte sobre algo que no está en sus notas. -- **web_scrape**: Extrae el contenido de una página web como markdown. Úsalo cuando web_search devuelva una URL que quieras leer. - -## Reglas de uso de herramientas -- Ya tienes contexto de las notas del usuario arriba. Solo usa herramientas si necesitas información más específica. -- Nunca inventes IDs de notas, URLs o IDs de libreta. Usa los IDs proporcionados en el contexto o en los resultados de herramientas. -- Para preguntas conversacionales simples (saludos, opiniones, conocimiento general), responde directamente sin herramientas.`, - }, - de: { - contextWithNotes: `## Notizen des Benutzers\n\n${contextNotes}\n\nWenn du Infos aus den obigen Notizen verwendest, zitiere den Titel der Quellnotiz in Klammern. Nicht Wort für Wort kopieren — umformulieren. Wenn die Notizen das Thema nicht abdecken, sag es und ergänze mit deinem Allgemeinwissen.`, - contextNoNotes: "Keine relevanten Notizen für diese Frage gefunden. Antworte mit deinem Allgemeinwissen.", - system: `Du bist der KI-Assistent von Memento. Der Benutzer stellt dir Fragen zu seinen Projekten, technischen Dokumentationen und Notizen. Du musst strukturiert und hilfreich antworten. - -## Formatregeln -- Verwende Markdown frei: Überschriften (##, ###), Listen, Code-Blöcke, Fettdruck, Tabellen. -- Strukturiere deine Antwort mit Abschnitten bei technischen Fragen oder komplexen Themen. -- Bei einfachen, kurzen Fragen reicht ein direkter Absatz. - -## Tonregeln -- Natürlicher Ton, weder zu geschäftsmäßig noch zu umgangssprachlich. -- Keine unnötigen Einleitungssätze. Antworte direkt. -- Keine Upsell-Fragen am Ende. Gib nützliche Zusatzinfos einfach direkt. - -## Verfügbare Werkzeuge -- **note_search**: Durchsuche die Notizen des Benutzers nach Schlagwort oder Bedeutung. Verwende es, wenn der obige Kontext unzureichend ist. Wenn ein Notizbuch ausgewählt ist, gib dessen ID an, um die Ergebnisse einzuschränken. -- **note_read**: Lese eine bestimmte Notiz anhand ihrer ID. Verwende es, wenn note_search eine Notiz zurückgibt, deren vollständigen Inhalt du benötigst. -- **web_search**: Suche im Web. Verwende es, wenn der Benutzer nach etwas fragt, das nicht in seinen Notizen steht. -- **web_scrape**: Lese eine Webseite und gib den Inhalt als Markdown zurück. Verwende es, wenn web_search eine URL zurückgibt, die du lesen möchtest. - -## Werkzeugregeln -- Du hast bereits Kontext aus den Notizen des Benutzers oben. Verwende Werkzeuge nur, wenn du spezifischere Informationen benötigst. -- Erfinde niemals Notiz-IDs, URLs oder Notizbuch-IDs. Verwende die im Kontext oder in Werkzeugergebnissen bereitgestellten IDs. -- Bei einfachen Gesprächsfragen (Begrüßungen, Meinungen, Allgemeinwissen) antworte direkt ohne Werkzeuge.`, - }, - it: { - contextWithNotes: `## Note dell'utente\n\n${contextNotes}\n\nQuando usi informazioni dalle note sopra, cita il titolo della nota fonte tra parentesi. Non copiare parola per parola — riformula. Se le note non coprono l'argomento, dillo e integra con la tua conoscenza generale.`, - contextNoNotes: "Nessuna nota rilevante trovata per questa domanda. Rispondi con la tua conoscenza generale.", - system: `Sei l'assistente IA di Memento. L'utente ti fa domande sui suoi progetti, documentazione tecnica e note. Devi rispondere in modo strutturato e utile. - -## Regole di formato -- Usa markdown liberamente: titoli (##, ###), elenchi, blocchi di codice, grassetto, tabelle. -- Struttura la risposta con sezioni per domande tecniche o argomenti complessi. -- Per domande semplici e brevi, un paragrafo diretto basta. - -## Regole di tono -- Tono naturale, né aziendale né troppo informale. -- Nessuna frase introduttiva non necessaria. Rispondi direttamente. -- Nessuna domanda di upsell alla fine. Se hai informazioni aggiuntive utili, dalle direttamente. - -## Strumenti disponibili -- **note_search**: Cerca nelle note dell'utente per parola chiave o significato. Usa quando il contesto iniziale è insufficiente. Se un quaderno è selezionato, passa il suo ID per restringere i risultati. -- **note_read**: Leggi una nota specifica per ID. Usa quando note_search restituisce una nota di cui hai bisogno del contenuto completo. -- **web_search**: Cerca sul web. Usa quando l'utente chiede qualcosa che non è nelle sue note. -- **web_scrape**: Estrai il contenuto di una pagina web come markdown. Usa quando web_search restituisce un URL che vuoi leggere. - -## Regole di utilizzo degli strumenti -- Hai già contesto dalle note dell'utente sopra. Usa gli strumenti solo se hai bisogno di informazioni più specifiche. -- Non inventare mai ID di note, URL o ID di quaderno. Usa gli ID forniti nel contesto o nei risultati degli strumenti. -- Per domande conversazionali semplici (saluti, opinioni, conoscenza generale), rispondi direttamente senza strumenti.`, +## EJEMPLO DE SALIDA HTML +

            Título de sección

            +

            Aquí hay una explicación con texto en negrita y una lista:

            +
              +
            • Primer punto importante
            • +
            • Segundo punto importante
            • +
            ` : ''), }, } - // Fallback to English if language not supported const prompts = promptLang[lang] || promptLang.en - const contextBlock = contextNotes.length > 0 - ? prompts.contextWithNotes - : prompts.contextNoNotes + const contextBlock = contextNotes.length > 0 ? prompts.contextWithNotes : prompts.contextNoNotes - // Load note images as base64 for vision-capable models + // Load note images for vision let imageContextParts: Array<{ type: 'image'; image: string }> = [] if (noteContext?.images && noteContext.images.length > 0) { for (const imgPath of noteContext.images.slice(0, 4)) { @@ -343,8 +249,7 @@ Tu as accès à ces outils pour des recherches approfondies : const buffer = await readFile(fullPath) const ext = path.extname(imgPath).toLowerCase() const mime = ext === '.png' ? 'image/png' : ext === '.gif' ? 'image/gif' : ext === '.webp' ? 'image/webp' : 'image/jpeg' - const base64 = `data:${mime};base64,${buffer.toString('base64')}` - imageContextParts.push({ type: 'image', image: base64 }) + imageContextParts.push({ type: 'image', image: `data:${mime};base64,${buffer.toString('base64')}` }) } catch {} } } @@ -352,113 +257,38 @@ Tu as accès à ces outils pour des recherches approfondies : let copilotContext = '' if (noteContext) { copilotContext = `\n\n## Current Note Context -You are currently helping the user edit a specific note. Here is the current content of the note: -Title: ${noteContext.title || 'Untitled'} - -Content: -${noteContext.content || '(empty)'} -${imageContextParts.length > 0 ? `\nImages: ${imageContextParts.length} image(s) attached. When the user asks about images, describe what you see in them.` : ''} - -The user wants you to write in a **${noteContext.tone || 'professional'}** tone. -IMPORTANT: Focus ONLY on this note. Do NOT reference other notes or external information unless the user explicitly asks. Your job is to help with this specific note — suggest rewrites, answer questions about it, or draft new sections.` +You are helping the user edit a specific note: ${noteContext.title || 'Untitled'}. +Tone: ${noteContext.tone || 'professional'}. +Content: ${noteContext.content || '(empty)'} +Focus ONLY on this note unless asked otherwise.` } - const systemPrompt = `${prompts.system} -${copilotContext} + const systemPrompt = `${prompts.system}\n${copilotContext}\n\n${contextBlock}\n\n## LANGUAGE RULE (MANDATORY)\nYou MUST respond in ${lang === 'en' ? 'English' : lang === 'fr' ? 'French' : lang === 'fa' ? 'Persian (Farsi)' : lang === 'es' ? 'Spanish' : 'English'}.` -${contextBlock} - -## LANGUAGE RULE (MANDATORY) -You MUST respond in ${lang === 'en' ? 'English' : lang === 'fr' ? 'French' : lang === 'fa' ? 'Persian (Farsi)' : lang === 'es' ? 'Spanish' : lang === 'de' ? 'German' : lang === 'it' ? 'Italian' : 'English'}. -Never switch to another language. Even if the user writes in a different language, respond in the configured language.` - - // 6. Build message history from DB + current messages - const dbHistory = conversation.messages.map((m: { role: string; content: string }) => ({ - role: m.role as 'user' | 'assistant' | 'system', - content: m.content, - })) - - // Only add the current user message if it's not already in DB history - const lastIncoming = incomingMessages[incomingMessages.length - 1] - const currentDbMessage = dbHistory[dbHistory.length - 1] - const isNewMessage = - lastIncoming && - (!currentDbMessage || - currentDbMessage.role !== 'user' || - currentDbMessage.content !== lastIncoming.content) - - let allMessages: Array<{ role: 'user' | 'assistant' | 'system'; content: string | Array }> = isNewMessage - ? [...dbHistory, { role: lastIncoming.role, content: lastIncoming.content }] - : dbHistory - - // Inject note images as a context message for vision models - if (imageContextParts.length > 0) { - allMessages = [ - { role: 'user', content: [{ type: 'text' as const, text: '[Attached note images — use these when the user asks about images]' }, ...imageContextParts] }, - { role: 'assistant', content: 'Understood. I can see the attached images and will describe or analyze them when asked.' }, - ...allMessages, - ] - } - - // Sliding window: keep first 2 messages (context) + last 48 to avoid context overflow - const WINDOW = 50 - if (allMessages.length > WINDOW) { - allMessages = [...allMessages.slice(0, 2), ...allMessages.slice(-(WINDOW - 2))] - } - - // 7. Get chat provider model - const config = await getSystemConfig() - const provider = getChatProvider(config) - const model = provider.getModel() - - // 7b. Build chat tools - const chatToolContext = { - userId, - conversationId: conversation.id, - notebookId, - webSearch: !!webSearch, - config, - } - // When scoped to "this note", only provide web tools — no note_search/note_read - // to prevent the AI from pulling information from other notes + // 6. Execute stream + const sysConfig = await getSystemConfig() const chatTools = noteContext - ? toolRegistry.buildToolsForChat({ ...chatToolContext, webOnly: true }) - : toolRegistry.buildToolsForChat(chatToolContext) + ? toolRegistry.buildToolsForChat({ userId, config: sysConfig, webSearch, webOnly: true }) + : toolRegistry.buildToolsForChat({ userId, config: sysConfig, webSearch }) - // 8. Save user message to DB before streaming - if (isNewMessage && lastIncoming) { - await prisma.chatMessage.create({ - data: { - conversationId: conversation.id, - role: 'user', - content: lastIncoming.content, - }, - }) - } - - // 9. Stream response - const result = streamText({ - model, + const provider = getChatProvider(sysConfig) + const result = await streamText({ + model: provider.chatModel, system: systemPrompt, - messages: allMessages as any, + messages: incomingMessages, tools: chatTools, - stopWhen: stepCountIs(5), - async onFinish({ text }) { - // Save assistant message to DB after streaming completes - await prisma.chatMessage.create({ - data: { - conversationId: conversation.id, - role: 'assistant', - content: text, - }, + maxSteps: 5, + onFinish: async (final) => { + // Save messages to DB + const userContent = incomingMessages[incomingMessages.length - 1].content + await prisma.message.create({ + data: { conversationId: conversation.id, role: 'user', content: userContent } }) - }, + await prisma.message.create({ + data: { conversationId: conversation.id, role: 'assistant', content: final.text } + }) + } }) - // 10. Return streaming response with conversation ID header - return result.toUIMessageStreamResponse({ - headers: { - 'X-Conversation-Id': conversation.id, - }, - }) + return result.toDataStreamResponse() } diff --git a/memento-note/app/globals.css b/memento-note/app/globals.css index a370058..bb00d91 100644 --- a/memento-note/app/globals.css +++ b/memento-note/app/globals.css @@ -16,20 +16,20 @@ --color-memento-desk: #E5E2D9; --color-memento-paper: #F2F0E9; --color-memento-ink: #1C1C1C; - --color-primary: #1C1C1C; - --color-memento-accent: #1C1C1C; + --color-primary: #ACB995; + --color-memento-accent: #D4A373; --color-memento-paper-elevated: #faf9f5; --color-background-light: var(--color-memento-paper); --color-background-dark: #202020; --font-sans: var(--font-inter); --font-heading: var(--font-memento-serif), ui-serif, Georgia, "Times New Roman", serif; - --shadow-level-1: 0 2px 4px rgba(0,0,0,0.04), 0 4px 8px rgba(0,0,0,0.06); - --shadow-level-2: 0 2px 8px rgba(0,0,0,0.06), 0 8px 16px rgba(0,0,0,0.08); - --shadow-level-3: 0 4px 8px rgba(0,0,0,0.04), 0 16px 32px rgba(0,0,0,0.12); - --shadow-acrylic: 0 8px 32px rgba(0,0,0,0.08); - --shadow-card-rest: 0 1px 2px rgba(0,0,0,0.06), 0 2px 4px rgba(0,0,0,0.04); - --shadow-card-hover: 0 2px 8px rgba(0,0,0,0.08), 0 8px 16px rgba(0,0,0,0.06); + --shadow-level-1: 0 2px 4px rgba(0, 0, 0, 0.04), 0 4px 8px rgba(0, 0, 0, 0.06); + --shadow-level-2: 0 2px 8px rgba(0, 0, 0, 0.06), 0 8px 16px rgba(0, 0, 0, 0.08); + --shadow-level-3: 0 4px 8px rgba(0, 0, 0, 0.04), 0 16px 32px rgba(0, 0, 0, 0.12); + --shadow-acrylic: 0 8px 32px rgba(0, 0, 0, 0.08); + --shadow-card-rest: 0 1px 2px rgba(0, 0, 0, 0.06), 0 2px 4px rgba(0, 0, 0, 0.04); + --shadow-card-hover: 0 2px 8px rgba(0, 0, 0, 0.08), 0 8px 16px rgba(0, 0, 0, 0.06); --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); --transition-normal: 250ms cubic-bezier(0.4, 0, 0.2, 1); } @@ -94,18 +94,22 @@ @utility font-memento-serif { font-family: var(--font-memento-serif), ui-serif, Georgia, "Times New Roman", serif; } + @utility acrylic-dark { background: rgba(32, 32, 32, 0.75); backdrop-filter: blur(20px) saturate(1.5); -webkit-backdrop-filter: blur(20px) saturate(1.5); } + @utility win11-shadow { box-shadow: var(--shadow-card-rest); transition: box-shadow var(--transition-normal); } + @utility win11-shadow-hover { box-shadow: var(--shadow-card-hover); } + @utility editor-body { font-size: var(--editor-body-size, 16px); } @@ -116,6 +120,7 @@ background-image: url("https://www.transparenttextures.com/patterns/natural-paper.png"); background-size: auto; } + html.dark .memento-paper-texture { background-image: none; } @@ -123,6 +128,7 @@ html.dark .memento-paper-texture { .memento-sidebar-depth { box-shadow: 1px 0 10px rgba(0, 0, 0, 0.05); } + html.dark .memento-sidebar-depth { box-shadow: 4px 0 24px -8px rgba(0, 0, 0, 0.4); } @@ -136,10 +142,12 @@ html:not(.dark) .memento-active-nav { .custom-scrollbar::-webkit-scrollbar { width: 3px; } + .custom-scrollbar::-webkit-scrollbar-thumb { background: rgba(28, 28, 28, 0.08); border-radius: 10px; } + .custom-scrollbar:hover::-webkit-scrollbar-thumb { background: rgba(28, 28, 28, 0.2); } @@ -164,7 +172,7 @@ html:not(.dark) .memento-active-nav { --ink: var(--foreground); --paper: var(--background); --muted-ink: var(--muted-foreground); - --ai-accent: #75B2D6; + --ai-accent: #ACB995; } /* Sidebar toggle view button (Notebooks / Agents) */ @@ -175,15 +183,18 @@ html:not(.dark) .memento-active-nav { border-radius: 999px; border: 1px solid var(--border); } + .sidebar-view-toggle-btn { padding: 6px; border-radius: 999px; transition: all 150ms; color: var(--muted-foreground); } + .sidebar-view-toggle-btn:hover { color: var(--foreground); } + .sidebar-view-toggle-btn.active { background: var(--foreground); color: var(--background); @@ -193,6 +204,7 @@ html:not(.dark) .memento-active-nav { html.dark .sidebar-view-toggle { background: rgba(255, 255, 255, 0.08); } + html.dark .sidebar-view-toggle-btn.active { background: var(--primary); color: var(--primary-foreground); @@ -209,17 +221,21 @@ html.dark .sidebar-view-toggle-btn.active { width: 100%; text-align: left; } + .sidebar-inbox-item:hover { background: rgba(255, 255, 255, 0.4); } + .sidebar-inbox-item.active { background: linear-gradient(to right, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.4)); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); border: 1px solid rgba(255, 255, 255, 0.5); } + html.dark .sidebar-inbox-item:hover { background: rgba(255, 255, 255, 0.06); } + html.dark .sidebar-inbox-item.active { background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.12); @@ -253,10 +269,12 @@ html.dark .sidebar-inbox-item.active { padding: 8px; transition: transform 100ms, opacity 100ms; } + .ai-send-btn:hover { opacity: 0.9; transform: scale(1.05); } + .ai-send-btn:active { transform: scale(0.95); } @@ -326,38 +344,37 @@ html.dark .memento-active-nav { :root { --radius: 0.5rem; - --memento-desk: #E5E2D9; - --background: #F2F0E9; - --foreground: #1C1C1C; - --card: #faf9f5; - --card-foreground: #1C1C1C; - --popover: #faf9f5; - --popover-foreground: #1C1C1C; - --primary: #1C1C1C; - --primary-foreground: #F2F0E9; - --secondary: #e8e6df; - --secondary-foreground: #1C1C1C; - --muted: #e3e1d8; - --muted-foreground: rgba(28, 28, 28, 0.58); - --accent: #ebe9e2; - --accent-foreground: #1C1C1C; - --destructive: #c42b1c; - --border: rgba(28, 28, 28, 0.1); - --input: rgba(28, 28, 28, 0.12); - --ring: rgba(28, 28, 28, 0.35); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: color-mix(in oklab, #ffffff 65%, #F2F0E9); - --sidebar-foreground: #1C1C1C; - --sidebar-primary: #1C1C1C; - --sidebar-primary-foreground: #F2F0E9; - --sidebar-accent: rgba(255, 255, 255, 0.45); - --sidebar-accent-foreground: #1C1C1C; - --sidebar-border: rgba(28, 28, 28, 0.1); - --sidebar-ring: rgba(28, 28, 28, 0.35); + --memento-desk: #F9F8F6; + --background: #F9F8F6; + --foreground: #212529; + --card: #ffffff; + --card-foreground: #212529; + --popover: #ffffff; + --popover-foreground: #212529; + --primary: #ACB995; + --primary-foreground: #F9F8F6; + --secondary: #75B2D6; + --secondary-foreground: #212529; + --muted: #75B2D6; + --muted-foreground: rgba(33, 37, 41, 0.6); + --accent: #75B2D6; + --accent-foreground: #212529; + --destructive: #E11D48; + --border: #75B2D6; + --input: #75B2D6; + --ring: rgba(33, 37, 41, 0.3); + --ai-accent: #ACB995; + --pinned-gold: #F59E0B; + --sage-green: #10B981; + + --sidebar: #ffffff; + --sidebar-foreground: #212529; + --sidebar-primary: #212529; + --sidebar-primary-foreground: #F9F8F6; + --sidebar-accent: #F9F8F6; + --sidebar-accent-foreground: #212529; + --sidebar-border: #75B2D6; + --sidebar-ring: rgba(33, 37, 41, 0.2); } html:not(.dark) { @@ -376,7 +393,7 @@ html.dark { --card-foreground: #1C1C1C; --popover: #faf9f5; --popover-foreground: #1C1C1C; - --primary: #1C1C1C; + --primary: #ACB995; --primary-foreground: #F2F0E9; --secondary: #e8e6df; --secondary-foreground: #1C1C1C; @@ -462,12 +479,17 @@ html.dark { } [data-theme='midnight'] { - --background: oklch(0.94 0.005 250); /* Gris-bleu très pâle */ - --foreground: oklch(0.18 0.03 250); /* Gris-bleu très foncé */ - --card: oklch(0.97 0.006 250); /* Gris-bleu pâle */ + --background: oklch(0.94 0.005 250); + /* Gris-bleu très pâle */ + --foreground: oklch(0.18 0.03 250); + /* Gris-bleu très foncé */ + --card: oklch(0.97 0.006 250); + /* Gris-bleu pâle */ --card-foreground: oklch(0.18 0.03 250); - --primary: oklch(0.5 0.12 250); /* Gris-bleu saturé */ - --primary-foreground: oklch(0.99 0 0); /* Blanc */ + --primary: oklch(0.5 0.12 250); + /* Gris-bleu saturé */ + --primary-foreground: oklch(0.99 0 0); + /* Blanc */ --secondary: oklch(0.2 0.01 250); --secondary-foreground: oklch(0.18 0.03 250); --muted: oklch(0.22 0.01 250); @@ -491,12 +513,17 @@ html.dark { } [data-theme='midnight'].dark { - --background: oklch(0.1 0.01 250); /* Noir profond */ - --foreground: oklch(0.96 0.005 250); /* Blanc grisâtre */ - --card: oklch(0.15 0.015 250); /* Gris-bleu très foncé */ + --background: oklch(0.1 0.01 250); + /* Noir profond */ + --foreground: oklch(0.96 0.005 250); + /* Blanc grisâtre */ + --card: oklch(0.15 0.015 250); + /* Gris-bleu très foncé */ --card-foreground: oklch(0.96 0.005 250); - --primary: oklch(0.6 0.12 250); /* Gris-bleu vibrant */ - --primary-foreground: oklch(0.1 0 0); /* Noir */ + --primary: oklch(0.6 0.12 250); + /* Gris-bleu vibrant */ + --primary-foreground: oklch(0.1 0 0); + /* Noir */ --secondary: oklch(0.18 0.015 250); --secondary-foreground: oklch(0.96 0.005 250); --muted: oklch(0.2 0.015 250); @@ -520,12 +547,17 @@ html.dark { } [data-theme='blue'] { - --background: oklch(0.985 0.005 225); /* Blanc légèrement bleuté */ - --foreground: oklch(0.18 0.035 225); /* Gris-bleu foncé saturé */ - --card: oklch(1 0 0); /* Blanc pur */ + --background: oklch(0.985 0.005 225); + /* Blanc légèrement bleuté */ + --foreground: oklch(0.18 0.035 225); + /* Gris-bleu foncé saturé */ + --card: oklch(1 0 0); + /* Blanc pur */ --card-foreground: oklch(0.18 0.035 225); - --primary: oklch(0.5 0.15 225); /* Bleu vibrant */ - --primary-foreground: oklch(0.99 0 0); /* Blanc */ + --primary: oklch(0.5 0.15 225); + /* Bleu vibrant */ + --primary-foreground: oklch(0.99 0 0); + /* Blanc */ --secondary: oklch(0.93 0.008 225); --secondary-foreground: oklch(0.18 0.035 225); --muted: oklch(0.9 0.01 225); @@ -549,12 +581,17 @@ html.dark { } [data-theme='blue'].dark { - --background: oklch(0.13 0.008 225); /* Noir légèrement bleuté */ - --foreground: oklch(0.97 0.006 225); /* Blanc légèrement bleuté */ - --card: oklch(0.17 0.01 225); /* Gris-bleu foncé */ + --background: oklch(0.13 0.008 225); + /* Noir légèrement bleuté */ + --foreground: oklch(0.97 0.006 225); + /* Blanc légèrement bleuté */ + --card: oklch(0.17 0.01 225); + /* Gris-bleu foncé */ --card-foreground: oklch(0.97 0.006 225); - --primary: oklch(0.6 0.15 225); /* Bleu vibrant plus clair */ - --primary-foreground: oklch(0.1 0 0); /* Noir */ + --primary: oklch(0.6 0.15 225); + /* Bleu vibrant plus clair */ + --primary-foreground: oklch(0.1 0 0); + /* Noir */ --secondary: oklch(0.22 0.015 225); --secondary-foreground: oklch(0.97 0.006 225); --muted: oklch(0.25 0.02 225); @@ -578,12 +615,17 @@ html.dark { } [data-theme='sepia'] { - --background: oklch(0.985 0.004 45); /* Blanc légèrement doré */ - --foreground: oklch(0.2 0.015 45); /* Gris-brun foncé */ - --card: oklch(1 0 0); /* Blanc pur */ + --background: oklch(0.985 0.004 45); + /* Blanc légèrement doré */ + --foreground: oklch(0.2 0.015 45); + /* Gris-brun foncé */ + --card: oklch(1 0 0); + /* Blanc pur */ --card-foreground: oklch(0.2 0.015 45); - --primary: oklch(0.45 0.08 45); /* Gris-brun chaud */ - --primary-foreground: oklch(0.99 0 0); /* Blanc */ + --primary: oklch(0.45 0.08 45); + /* Gris-brun chaud */ + --primary-foreground: oklch(0.99 0 0); + /* Blanc */ --secondary: oklch(0.94 0.008 45); --secondary-foreground: oklch(0.2 0.015 45); --muted: oklch(0.91 0.01 45); @@ -607,12 +649,17 @@ html.dark { } [data-theme='sepia'].dark { - --background: oklch(0.15 0.008 45); /* Noir légèrement bruni */ - --foreground: oklch(0.97 0.005 45); /* Blanc légèrement bruni */ - --card: oklch(0.19 0.01 45); /* Gris-brun foncé */ + --background: oklch(0.15 0.008 45); + /* Noir légèrement bruni */ + --foreground: oklch(0.97 0.005 45); + /* Blanc légèrement bruni */ + --card: oklch(0.19 0.01 45); + /* Gris-brun foncé */ --card-foreground: oklch(0.97 0.005 45); - --primary: oklch(0.55 0.08 45); /* Gris-brun plus clair */ - --primary-foreground: oklch(0.1 0 0); /* Noir */ + --primary: oklch(0.55 0.08 45); + /* Gris-brun plus clair */ + --primary-foreground: oklch(0.1 0 0); + /* Noir */ --secondary: oklch(0.25 0.015 45); --secondary-foreground: oklch(0.97 0.005 45); --muted: oklch(0.23 0.02 45); @@ -668,6 +715,7 @@ html.dark { --sidebar-border: oklch(0.88 0.02 350); --sidebar-ring: oklch(0.6 0.015 350); } + [data-theme='rose'].dark { --background: oklch(0.14 0.01 350); --foreground: oklch(0.95 0.01 350); @@ -726,6 +774,7 @@ html.dark { --sidebar-border: oklch(0.88 0.02 155); --sidebar-ring: oklch(0.6 0.015 155); } + [data-theme='green'].dark { --background: oklch(0.14 0.01 155); --foreground: oklch(0.95 0.01 155); @@ -784,6 +833,7 @@ html.dark { --sidebar-border: oklch(0.88 0.02 290); --sidebar-ring: oklch(0.6 0.015 290); } + [data-theme='lavender'].dark { --background: oklch(0.14 0.01 290); --foreground: oklch(0.95 0.01 290); @@ -842,6 +892,7 @@ html.dark { --sidebar-border: oklch(0.88 0.015 70); --sidebar-ring: oklch(0.6 0.015 60); } + [data-theme='sand'].dark { --background: oklch(0.15 0.01 60); --foreground: oklch(0.95 0.01 60); @@ -900,6 +951,7 @@ html.dark { --sidebar-border: oklch(0.88 0.015 195); --sidebar-ring: oklch(0.6 0.015 195); } + [data-theme='ocean'].dark { --background: oklch(0.14 0.01 195); --foreground: oklch(0.95 0.01 195); @@ -958,6 +1010,7 @@ html.dark { --sidebar-border: oklch(0.88 0.02 40); --sidebar-ring: oklch(0.6 0.02 35); } + [data-theme='sunset'].dark { --background: oklch(0.15 0.012 35); --foreground: oklch(0.95 0.01 35); @@ -992,6 +1045,7 @@ html.dark { html.font-system { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif !important; } + html.font-system body, html.font-system * { font-family: inherit !important; @@ -1121,7 +1175,9 @@ html.font-system * { caret-color: var(--primary); } -.notion-editor-wrapper .ProseMirror > *:first-child { margin-top: 0; } +.notion-editor-wrapper .ProseMirror>*:first-child { + margin-top: 0; +} /* Placeholder */ .notion-editor-wrapper .ProseMirror p.is-editor-empty:first-child::before, @@ -1136,7 +1192,9 @@ html.font-system * { } /* --- Paragraphs --- */ -.notion-editor-wrapper .ProseMirror p { margin: 0.2em 0; } +.notion-editor-wrapper .ProseMirror p { + margin: 0.2em 0; +} /* --- Headings (Notion style — clean, sans-serif) --- */ .notion-editor-wrapper .ProseMirror h1 { @@ -1146,6 +1204,7 @@ html.font-system * { line-height: 1.25; letter-spacing: -0.02em; } + .notion-editor-wrapper .ProseMirror h2 { font-size: 1.375rem; font-weight: 600; @@ -1153,6 +1212,7 @@ html.font-system * { line-height: 1.3; letter-spacing: -0.01em; } + .notion-editor-wrapper .ProseMirror h3 { font-size: 1.125rem; font-weight: 600; @@ -1166,56 +1226,77 @@ html.font-system * { padding-left: 1.5rem; margin: 0.25em 0; } + .notion-editor-wrapper .ProseMirror ol { list-style-type: decimal; padding-left: 1.5rem; margin: 0.25em 0; } -.notion-editor-wrapper .ProseMirror li > p { margin: 0.1em 0; } -.notion-editor-wrapper .ProseMirror li > ul, -.notion-editor-wrapper .ProseMirror li > ol { margin: 0.1em 0; } + +.notion-editor-wrapper .ProseMirror li>p { + margin: 0.1em 0; +} + +.notion-editor-wrapper .ProseMirror li>ul, +.notion-editor-wrapper .ProseMirror li>ol { + margin: 0.1em 0; +} /* --- Task / Todo List --- */ .notion-editor-wrapper .ProseMirror ul[data-type="taskList"] { list-style: none; padding-left: 0; } + .notion-editor-wrapper .ProseMirror ul[data-type="taskList"] li { display: flex; align-items: flex-start; gap: 0.4rem; margin: 0.2em 0; } -.notion-editor-wrapper .ProseMirror ul[data-type="taskList"] li > label { + +.notion-editor-wrapper .ProseMirror ul[data-type="taskList"] li>label { display: flex; align-items: center; cursor: pointer; margin-top: 2px; } -.notion-editor-wrapper .ProseMirror ul[data-type="taskList"] li > label input[type="checkbox"] { - -webkit-appearance: none; appearance: none; - width: 16px; height: 16px; + +.notion-editor-wrapper .ProseMirror ul[data-type="taskList"] li>label input[type="checkbox"] { + -webkit-appearance: none; + appearance: none; + width: 16px; + height: 16px; border: 2px solid var(--border); border-radius: 4px; cursor: pointer; - display: grid; place-content: center; + display: grid; + place-content: center; transition: all 0.15s ease; background: transparent; flex-shrink: 0; } -.notion-editor-wrapper .ProseMirror ul[data-type="taskList"] li > label input[type="checkbox"]:checked { + +.notion-editor-wrapper .ProseMirror ul[data-type="taskList"] li>label input[type="checkbox"]:checked { background: var(--primary); border-color: var(--primary); } -.notion-editor-wrapper .ProseMirror ul[data-type="taskList"] li > label input[type="checkbox"]::before { - content: ""; width: 10px; height: 10px; + +.notion-editor-wrapper .ProseMirror ul[data-type="taskList"] li>label input[type="checkbox"]::before { + content: ""; + width: 10px; + height: 10px; transform: scale(0); transition: 120ms transform ease-in-out; box-shadow: inset 1em 1em white; clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%); } -.notion-editor-wrapper .ProseMirror ul[data-type="taskList"] li > label input[type="checkbox"]:checked::before { transform: scale(1); } -.notion-editor-wrapper .ProseMirror ul[data-type="taskList"] li[data-checked="true"] > div > p { + +.notion-editor-wrapper .ProseMirror ul[data-type="taskList"] li>label input[type="checkbox"]:checked::before { + transform: scale(1); +} + +.notion-editor-wrapper .ProseMirror ul[data-type="taskList"] li[data-checked="true"]>div>p { color: var(--muted-foreground); text-decoration: line-through; } @@ -1240,6 +1321,7 @@ html.font-system * { font-family: var(--font-mono, ui-monospace, monospace); color: var(--foreground); } + .notion-editor-wrapper .ProseMirror pre { background: var(--muted); border-radius: 8px; @@ -1248,6 +1330,7 @@ html.font-system * { overflow-x: auto; border: 1px solid var(--border); } + .notion-editor-wrapper .ProseMirror pre code { background: none; padding: 0; @@ -1264,7 +1347,9 @@ html.font-system * { } /* --- Strong / Marks --- */ -.notion-editor-wrapper .ProseMirror strong { font-weight: 600; } +.notion-editor-wrapper .ProseMirror strong { + font-weight: 600; +} /* --- Images --- */ .notion-editor-wrapper .ProseMirror img { @@ -1275,7 +1360,11 @@ html.font-system * { margin: 0.5em 0; transition: filter 0.15s ease; } -.notion-editor-wrapper .ProseMirror img:hover { filter: brightness(95%); } + +.notion-editor-wrapper .ProseMirror img:hover { + filter: brightness(95%); +} + .notion-editor-wrapper .ProseMirror img.ProseMirror-selectednode { outline: 2px solid var(--primary); outline-offset: 2px; @@ -1288,6 +1377,7 @@ html.font-system * { text-underline-offset: 2px; cursor: pointer; } + .notion-editor-wrapper .ProseMirror a:hover { opacity: 0.8; } @@ -1298,10 +1388,66 @@ html.font-system * { border-radius: 2px; padding: 0 1px; } + .dark .notion-editor-wrapper .ProseMirror mark { background: oklch(0.4 0.1 90); } +/* --- Tables --- */ +.notion-editor-wrapper .ProseMirror table { + border-collapse: collapse; + margin: 1em 0; + overflow: hidden; + table-layout: fixed; + width: 100%; +} + +.notion-editor-wrapper .ProseMirror td, +.notion-editor-wrapper .ProseMirror th { + border: 1px solid var(--border); + box-sizing: border-box; + min-width: 1em; + padding: 8px 12px; + position: relative; + vertical-align: top; +} + +.notion-editor-wrapper .ProseMirror th { + background-color: var(--muted); + font-weight: 600; + text-align: left; +} + +.notion-editor-wrapper .ProseMirror .selectedCell:after { + background: rgba(200, 200, 255, 0.4); + content: ""; + left: 0; + right: 0; + top: 0; + bottom: 0; + pointer-events: none; + position: absolute; + z-index: 2; +} + +.notion-editor-wrapper .ProseMirror .column-resize-handle { + background-color: var(--primary); + bottom: -2px; + position: absolute; + right: -2px; + top: 0; + width: 4px; +} + +.notion-editor-wrapper .ProseMirror .tableWrapper { + overflow-x: auto; +} + +.notion-editor-wrapper .ProseMirror.resize-cursor { + cursor: ew-resize; + cursor: col-resize; +} + /* ============================================ Bubble Menu (floating toolbar on selection) ============================================ */ @@ -1309,15 +1455,16 @@ html.font-system * { background: var(--popover); border: 1px solid var(--border); border-radius: 10px; - box-shadow: 0 2px 8px rgba(0,0,0,0.08), 0 8px 16px rgba(0,0,0,0.06); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08), 0 8px 16px rgba(0, 0, 0, 0.06); padding: 2px; display: flex; align-items: center; gap: 0; z-index: 100; } + .dark .notion-bubble-menu { - box-shadow: 0 2px 8px rgba(0,0,0,0.3), 0 8px 24px rgba(0,0,0,0.4); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3), 0 8px 24px rgba(0, 0, 0, 0.4); } .notion-bubble-btn { @@ -1333,12 +1480,15 @@ html.font-system * { cursor: pointer; transition: all 0.15s ease; } + .notion-bubble-btn:hover { background: var(--accent); } + .notion-bubble-btn:hover { background: var(--accent); } + .notion-bubble-btn-active { background: var(--accent); color: var(--primary); @@ -1353,14 +1503,16 @@ html.font-system * { background: var(--popover); border: 1px solid var(--border); border-radius: 8px; - box-shadow: 0 2px 8px rgba(0,0,0,0.04), 0 8px 24px rgba(0,0,0,0.08); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04), 0 8px 24px rgba(0, 0, 0, 0.08); padding: 4px; min-width: 150px; z-index: 110; } + .dark .notion-ai-submenu { - box-shadow: 0 8px 24px rgba(0,0,0,0.5); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); } + .notion-ai-subitem { display: flex; align-items: center; @@ -1375,9 +1527,11 @@ html.font-system * { color: var(--foreground); transition: background 0.15s ease; } + .notion-ai-subitem:hover { background: var(--accent); } + .notion-ai-lang-picker { border-top: 1px solid var(--border); margin-top: 4px; @@ -1386,6 +1540,7 @@ html.font-system * { gap: 3px; padding: 6px; } + .notion-ai-lang-item { font-size: 0.72rem; padding: 3px 8px; @@ -1396,35 +1551,48 @@ html.font-system * { color: var(--foreground); transition: background 0.1s; } -.notion-ai-lang-item:hover { background: var(--accent); } + +.notion-ai-lang-item:hover { + background: var(--accent); +} + .notion-ai-result-overlay { position: fixed; inset: 0; - background: rgba(0,0,0,0.45); + background: rgba(0, 0, 0, 0.45); z-index: 99998; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(2px); } + .notion-ai-result-modal { background: var(--popover); border: 1px solid var(--border); border-radius: 8px; - box-shadow: 0 2px 8px rgba(0,0,0,0.04), 0 16px 48px rgba(0,0,0,0.12); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04), 0 16px 48px rgba(0, 0, 0, 0.12); padding: 20px; width: min(520px, 90vw); max-height: 80vh; overflow-y: auto; } -.dark .notion-ai-result-modal { box-shadow: 0 20px 60px rgba(0,0,0,0.6); } + +.dark .notion-ai-result-modal { + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6); +} + .notion-ai-result-header { display: flex; align-items: center; gap: 8px; margin-bottom: 16px; } -.notion-ai-result-section { margin-bottom: 12px; } + +.notion-ai-result-section { + margin-bottom: 12px; +} + .notion-ai-result-label { font-size: 0.7rem; font-weight: 600; @@ -1444,6 +1612,7 @@ html.font-system * { width: 200px; min-width: 120px; } + .notion-inline-input::placeholder { color: var(--muted-foreground); opacity: 0.6; @@ -1457,23 +1626,27 @@ html.font-system * { display: flex; align-items: center; justify-content: center; - background: rgba(0,0,0,0.3); + background: rgba(0, 0, 0, 0.3); } + .dark .notion-overlay { - background: rgba(0,0,0,0.6); + background: rgba(0, 0, 0, 0.6); } + .notion-image-modal { background: var(--popover); border: 1px solid var(--border); border-radius: 8px; - box-shadow: 0 2px 8px rgba(0,0,0,0.04), 0 16px 48px rgba(0,0,0,0.08); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04), 0 16px 48px rgba(0, 0, 0, 0.08); padding: 20px; width: 400px; max-width: 90vw; } + .dark .notion-image-modal { - box-shadow: 0 16px 48px rgba(0,0,0,0.5); + box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5); } + .notion-modal-input { width: 100%; padding: 8px 12px; @@ -1485,14 +1658,17 @@ html.font-system * { outline: none; transition: border-color var(--transition-fast); } + .notion-modal-input:focus { border-color: var(--primary); box-shadow: 0 0 0 1px var(--primary); } + .notion-modal-input::placeholder { color: var(--muted-foreground); opacity: 0.5; } + .notion-modal-btn { padding: 6px 14px; border-radius: 4px; @@ -1503,14 +1679,17 @@ html.font-system * { cursor: pointer; transition: background var(--transition-fast); } + .notion-modal-btn:hover { background: var(--accent); } + .notion-modal-btn-primary { background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); } + .notion-modal-btn-primary:hover { opacity: 0.9; background: var(--primary); @@ -1525,7 +1704,7 @@ html.font-system * { background: var(--popover); border: 1px solid var(--border); border-radius: 8px; - box-shadow: 0 2px 8px rgba(0,0,0,0.04), 0 8px 32px rgba(0,0,0,0.08); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04), 0 8px 32px rgba(0, 0, 0, 0.08); padding: 6px; min-width: 320px; max-height: 420px; @@ -1534,12 +1713,21 @@ html.font-system * { scrollbar-color: var(--border) transparent; animation: slash-enter 0.2s cubic-bezier(0.4, 0, 0.2, 1); } + @keyframes slash-enter { - from { opacity: 0; transform: translateY(-4px) scale(0.98); } - to { opacity: 1; transform: translateY(0) scale(1); } + from { + opacity: 0; + transform: translateY(-4px) scale(0.98); + } + + to { + opacity: 1; + transform: translateY(0) scale(1); + } } + .dark .notion-slash-menu { - box-shadow: 0 2px 8px rgba(0,0,0,0.3), 0 8px 32px rgba(0,0,0,0.5); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3), 0 8px 32px rgba(0, 0, 0, 0.5); } /* Header hint */ @@ -1581,33 +1769,40 @@ html.font-system * { transition: all 0.15s ease; white-space: nowrap; } + .notion-slash-tab:hover { background: var(--accent); color: var(--foreground); } + .notion-slash-tab-active { background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); } + .notion-slash-tab-active:hover { opacity: 0.9; background: var(--primary); color: var(--primary-foreground); } + .notion-slash-tab-ai { border-color: oklch(0.82 0.08 270); color: oklch(0.52 0.18 270); } + .dark .notion-slash-tab-ai { border-color: oklch(0.38 0.09 270); color: oklch(0.75 0.16 270); } + .notion-slash-tab-ai.notion-slash-tab-active { background: oklch(0.55 0.18 270); border-color: oklch(0.55 0.18 270); color: white; } + .notion-slash-tab-count { display: inline-flex; align-items: center; @@ -1618,14 +1813,15 @@ html.font-system * { border-radius: 10px; font-size: 9px; font-weight: 700; - background: rgba(0,0,0,0.1); + background: rgba(0, 0, 0, 0.1); line-height: 1; } + .notion-slash-tab-active .notion-slash-tab-count { - background: rgba(255,255,255,0.25); + background: rgba(255, 255, 255, 0.25); } -.notion-slash-section + .notion-slash-section { +.notion-slash-section+.notion-slash-section { border-top: 1px solid var(--border); margin-top: 4px; padding-top: 4px; @@ -1640,9 +1836,11 @@ html.font-system * { color: var(--muted-foreground); user-select: none; } + .notion-slash-label-ai { color: oklch(0.55 0.15 270); } + .dark .notion-slash-label-ai { color: oklch(0.72 0.15 270); } @@ -1661,13 +1859,16 @@ html.font-system * { color: var(--foreground); transition: background 0.15s ease, transform 0.08s ease; } + [dir="rtl"] .notion-slash-item { text-align: right; } + .notion-slash-item:hover, .notion-slash-item-selected { background: var(--accent); } + .notion-slash-item-selected { transform: none; } @@ -1685,6 +1886,7 @@ html.font-system * { flex-shrink: 0; transition: background 0.15s ease; } + .notion-slash-item-selected .notion-slash-icon, .notion-slash-item:hover .notion-slash-icon { background: var(--popover); @@ -1695,6 +1897,7 @@ html.font-system * { border-color: oklch(0.82 0.08 270); color: oklch(0.52 0.18 270); } + .dark .notion-slash-icon-ai { background: oklch(0.22 0.05 270); border-color: oklch(0.38 0.09 270); @@ -1718,6 +1921,7 @@ html.font-system * { text-overflow: ellipsis; line-height: 1.3; } + .notion-slash-desc { font-size: 11px; color: var(--muted-foreground); @@ -1752,21 +1956,56 @@ html.font-system * { /* ============================================ Note Card Rich Text Preview ============================================ */ -.rt-preview { font-size: 0.875rem; line-height: 1.6; } -.rt-preview h1 { font-size: 1.3rem; font-weight: 700; margin: 0.3em 0; } -.rt-preview h2 { font-size: 1.1rem; font-weight: 600; margin: 0.3em 0; } -.rt-preview h3 { font-size: 1rem; font-weight: 600; margin: 0.3em 0; } -.rt-preview p { margin: 0.2em 0; } -.rt-preview ul, .rt-preview ol { padding-left: 1.25rem; margin: 0.2em 0; } -.rt-preview blockquote { border-left: 3px solid var(--border); padding-left: 0.75rem; color: var(--muted-foreground); margin: 0.3em 0; } +.rt-preview { + font-size: 0.875rem; + line-height: 1.6; +} + +.rt-preview h1 { + font-size: 1.3rem; + font-weight: 700; + margin: 0.3em 0; +} + +.rt-preview h2 { + font-size: 1.1rem; + font-weight: 600; + margin: 0.3em 0; +} + +.rt-preview h3 { + font-size: 1rem; + font-weight: 600; + margin: 0.3em 0; +} + +.rt-preview p { + margin: 0.2em 0; +} + +.rt-preview ul, +.rt-preview ol { + padding-left: 1.25rem; + margin: 0.2em 0; +} + +.rt-preview blockquote { + border-left: 3px solid var(--border); + padding-left: 0.75rem; + color: var(--muted-foreground); + margin: 0.3em 0; +} + /* ── Full-page editorial editor: enforce prototype typography ──────────── */ /* TipTap ProseMirror text size — scoped to fullPage editor only */ .fullpage-editor .ProseMirror { - font-size: 1.125rem; /* 18px */ + font-size: 1.125rem; + /* 18px */ line-height: 1.875; color: var(--foreground); outline: none; } + .fullpage-editor .ProseMirror p { font-size: 1.125rem; line-height: 1.875; @@ -1774,12 +2013,39 @@ html.font-system * { color: var(--foreground); opacity: 0.85; } -.fullpage-editor .ProseMirror h1 { font-size: 2rem; line-height: 1.3; font-family: var(--font-memento-serif, Georgia, serif); margin-bottom: 0.75em; } -.fullpage-editor .ProseMirror h2 { font-size: 1.5rem; line-height: 1.35; font-family: var(--font-memento-serif, Georgia, serif); margin-bottom: 0.6em; } -.fullpage-editor .ProseMirror h3 { font-size: 1.25rem; line-height: 1.4; margin-bottom: 0.5em; } + +.fullpage-editor .ProseMirror h1 { + font-size: 2rem; + line-height: 1.3; + font-family: var(--font-memento-serif, Georgia, serif); + margin-bottom: 0.75em; +} + +.fullpage-editor .ProseMirror h2 { + font-size: 1.5rem; + line-height: 1.35; + font-family: var(--font-memento-serif, Georgia, serif); + margin-bottom: 0.6em; +} + +.fullpage-editor .ProseMirror h3 { + font-size: 1.25rem; + line-height: 1.4; + margin-bottom: 0.5em; +} + .fullpage-editor .ProseMirror ul, -.fullpage-editor .ProseMirror ol { font-size: 1.125rem; line-height: 1.8; padding-left: 1.5em; margin-bottom: 1em; } -.fullpage-editor .ProseMirror li { margin-bottom: 0.3em; } +.fullpage-editor .ProseMirror ol { + font-size: 1.125rem; + line-height: 1.8; + padding-left: 1.5em; + margin-bottom: 1em; +} + +.fullpage-editor .ProseMirror li { + margin-bottom: 0.3em; +} + .fullpage-editor .ProseMirror blockquote { border-left: 3px solid var(--foreground); padding-left: 1.25rem; @@ -1790,6 +2056,7 @@ html.font-system * { opacity: 0.7; margin: 1.5em 0; } + .fullpage-editor .ProseMirror p.is-editor-empty:first-child::before { color: var(--muted-foreground); content: attr(data-placeholder); @@ -1806,18 +2073,66 @@ html.font-system * { font-size: var(--editor-body-size, 16px) !important; line-height: 1.85 !important; } + /* Also target TipTap's actual editable div directly */ .fullpage-editor .tiptap, .fullpage-editor .tiptap p, .fullpage-editor .ProseMirror, -.fullpage-editor .ProseMirror > p { +.fullpage-editor .ProseMirror>p { font-size: var(--editor-body-size, 16px) !important; line-height: 1.85 !important; } + /* Keep headings at their correct relative sizes */ .fullpage-editor .tiptap h1, -.fullpage-editor .ProseMirror h1 { font-size: 2.25rem !important; line-height: 1.25 !important; } +.fullpage-editor .ProseMirror h1 { + font-size: 2.25rem !important; + line-height: 1.25 !important; +} + .fullpage-editor .tiptap h2, -.fullpage-editor .ProseMirror h2 { font-size: 1.75rem !important; line-height: 1.3 !important; } +.fullpage-editor .ProseMirror h2 { + font-size: 1.75rem !important; + line-height: 1.3 !important; +} + .fullpage-editor .tiptap h3, -.fullpage-editor .ProseMirror h3 { font-size: 1.375rem !important; line-height: 1.4 !important; } +.fullpage-editor .ProseMirror h3 { + font-size: 1.375rem !important; + line-height: 1.4 !important; +} + +/* ────────────────────────────────────────────── + SONNER TOASTS — Architectural Grid Styling + ────────────────────────────────────────────── */ +.memento-toast { + background: color-mix(in srgb, var(--card) 85%, transparent) !important; + backdrop-filter: blur(12px) !important; + -webkit-backdrop-filter: blur(12px) !important; + color: var(--foreground) !important; + border: 1px solid var(--border) !important; + font-family: var(--font-sans) !important; + border-radius: 14px !important; + box-shadow: var(--shadow-level-2) !important; + padding: 12px 16px !important; + font-size: 12px !important; + font-weight: 600 !important; + letter-spacing: -0.01em !important; +} + +.memento-toast-success { + border-color: var(--primary) !important; +} + +.memento-toast-error { + border-color: #fca5a5 !important; + /* Soft error red */ +} + +.memento-toast-info { + border-color: var(--ai-accent) !important; +} + +.memento-toast-warning { + border-color: var(--memento-accent) !important; +} \ No newline at end of file diff --git a/memento-note/components/ai-chat.tsx b/memento-note/components/ai-chat.tsx index ebf703b..df5eb7c 100644 --- a/memento-note/components/ai-chat.tsx +++ b/memento-note/components/ai-chat.tsx @@ -147,7 +147,7 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b return (