Compare commits

4 Commits

Author SHA1 Message Date
Antigravity
bbca93c4be feat: add reminder and move-to-notebook actions to editorial note menu
- Add Bell/reminder item to EditorialNoteMenu (notes-editorial-view.tsx)
- Add FolderOpen submenu for moving notes between notebooks
- Import ReminderDialog, useNotebooks, DropdownMenuSub components
- Fix settings/general/page.tsx to pass only required props to GeneralSettingsClient

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:18:44 +00:00
Antigravity
368b43cb8e feat: improve AI Chat UX, add notebook summary, and fix shared/reminders routing
Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 1m50s
2026-05-09 14:40:36 +00:00
Antigravity
66e957fd59 fix: add missing error handling for sendMessage promise rejections
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 14:15:38 +00:00
Antigravity
b0c2556a12 style: restore blue accents for AI dialog components and standardize gold header 2026-05-09 13:23:04 +00:00
32 changed files with 671 additions and 488 deletions

View File

@@ -103,9 +103,9 @@ export default function AITestPage() {
{/* 3. Chat Test - Horizontal Layout */} {/* 3. Chat Test - Horizontal Layout */}
<div className="bg-card rounded-[4rem] border border-border/60 shadow-xl overflow-hidden hover:shadow-2xl transition-all duration-700 group flex flex-col xl:flex-row"> <div className="bg-card rounded-[4rem] border border-border/60 shadow-xl overflow-hidden hover:shadow-2xl transition-all duration-700 group flex flex-col xl:flex-row">
<div className="xl:w-1/3 p-12 md:p-16 border-b xl:border-b-0 xl:border-r border-border/40 bg-gradient-to-br from-violet-500/[0.05] to-transparent relative overflow-hidden"> <div className="xl:w-1/3 p-12 md:p-16 border-b xl:border-b-0 xl:border-r border-border/40 bg-gradient-to-br from-zinc-500/[0.05] to-transparent relative overflow-hidden">
<div className="absolute -right-10 -bottom-10 opacity-[0.03] group-hover:opacity-[0.08] transition-all duration-700 group-hover:scale-125 group-hover:-rotate-6"> <div className="absolute -right-10 -bottom-10 opacity-[0.03] group-hover:opacity-[0.08] transition-all duration-700 group-hover:scale-125 group-hover:-rotate-6">
<MessageSquare className="h-80 w-80 text-violet-500" /> <MessageSquare className="h-80 w-80 text-zinc-500" />
</div> </div>
<div className="relative space-y-8"> <div className="relative space-y-8">
<div className="w-20 h-20 rounded-[1.5rem] bg-background flex items-center justify-center text-4xl shadow-2xl border border-border/50 group-hover:scale-110 transition-transform duration-500"> <div className="w-20 h-20 rounded-[1.5rem] bg-background flex items-center justify-center text-4xl shadow-2xl border border-border/50 group-hover:scale-110 transition-transform duration-500">
@@ -116,12 +116,12 @@ export default function AITestPage() {
<p className="text-lg text-muted-foreground font-bold opacity-80 leading-relaxed">{t('admin.aiTest.chatTestDescription')}</p> <p className="text-lg text-muted-foreground font-bold opacity-80 leading-relaxed">{t('admin.aiTest.chatTestDescription')}</p>
</div> </div>
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
<span className="px-4 py-2 bg-violet-500/10 rounded-xl text-violet-600 text-[10px] font-black uppercase tracking-widest">Conversational</span> <span className="px-4 py-2 bg-zinc-500/10 rounded-xl text-zinc-600 text-[10px] font-black uppercase tracking-widest">Conversational</span>
<span className="px-4 py-2 bg-violet-500/10 rounded-xl text-violet-600 text-[10px] font-black uppercase tracking-widest">Streaming</span> <span className="px-4 py-2 bg-zinc-500/10 rounded-xl text-zinc-600 text-[10px] font-black uppercase tracking-widest">Streaming</span>
</div> </div>
</div> </div>
</div> </div>
<div className="xl:w-2/3 p-12 md:p-16 bg-gradient-to-l from-transparent to-violet-500/[0.01]"> <div className="xl:w-2/3 p-12 md:p-16 bg-gradient-to-l from-transparent to-zinc-500/[0.01]">
<div className="max-w-4xl"> <div className="max-w-4xl">
<AI_TESTER type="chat" /> <AI_TESTER type="chat" />
</div> </div>

View File

@@ -736,7 +736,7 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
{/* Chat Provider */} {/* Chat Provider */}
<div className={`space-y-4 p-4 border border-border/50 rounded-lg bg-muted/50 ${activeAiTab === 'chat' ? 'block' : 'hidden'}`}> <div className={`space-y-4 p-4 border border-border/50 rounded-lg bg-muted/50 ${activeAiTab === 'chat' ? 'block' : 'hidden'}`}>
<h3 className="text-base font-semibold flex items-center gap-2"> <h3 className="text-base font-semibold flex items-center gap-2">
<span className="text-blue-600">💬</span> {t('admin.ai.chatProvider')} <span className="text-zinc-600">💬</span> {t('admin.ai.chatProvider')}
</h3> </h3>
<p className="text-xs text-muted-foreground">{t('admin.ai.chatDescription')}</p> <p className="text-xs text-muted-foreground">{t('admin.ai.chatDescription')}</p>

View File

@@ -31,12 +31,12 @@ export default async function MainLayout({
return ( return (
<ProvidersWrapper initialLanguage={initialLanguage} initialTranslations={initialTranslations}> <ProvidersWrapper initialLanguage={initialLanguage} initialTranslations={initialTranslations}>
{/* No top-bar header — sidebar-only navigation (architectural-grid design) */} {/* No top-bar header — sidebar-only navigation (architectural-grid design) */}
<div className="flex h-screen overflow-hidden bg-[#E5E2D9]"> <div className="flex h-screen overflow-hidden bg-memento-desk">
<Suspense fallback={<div className="hidden w-80 shrink-0 md:block" />}> <Suspense fallback={<div className="hidden w-80 shrink-0 md:block" />}>
<Sidebar user={session?.user} /> <Sidebar user={session?.user} />
</Suspense> </Suspense>
<main className="memento-paper-texture flex min-h-0 flex-1 flex-col overflow-y-auto scroll-smooth"> <main className="flex min-h-0 flex-1 flex-col overflow-y-auto scroll-smooth bg-memento-paper">
{children} {children}
</main> </main>

View File

@@ -25,6 +25,7 @@ export default async function HomePage() {
notesViewMode, notesViewMode,
noteHistory: settings?.noteHistory === true, noteHistory: settings?.noteHistory === true,
noteHistoryMode: (settings?.noteHistoryMode ?? 'manual') as 'manual' | 'auto', noteHistoryMode: (settings?.noteHistoryMode ?? 'manual') as 'manual' | 'auto',
aiAssistantEnabled: settings?.paragraphRefactor !== false,
}} }}
/> />
) )

View File

@@ -1,9 +0,0 @@
import { getNotesWithReminders } from '@/app/actions/notes'
import { RemindersPage } from '@/components/reminders-page'
export const dynamic = 'force-dynamic'
export default async function RemindersRoute() {
const notes = await getNotesWithReminders()
return <RemindersPage notes={notes} />
}

View File

@@ -127,7 +127,7 @@ export default function DataSettingsPage() {
{/* Export card */} {/* Export card */}
<div className="bg-card rounded-xl border border-border p-6 shadow-sm flex flex-col justify-between transition-all hover:shadow-md"> <div className="bg-card rounded-xl border border-border p-6 shadow-sm flex flex-col justify-between transition-all hover:shadow-md">
<div className="space-y-4"> <div className="space-y-4">
<div className="w-12 h-12 rounded-full bg-blue-500/10 flex items-center justify-center text-blue-600 shrink-0"> <div className="w-12 h-12 rounded-full bg-zinc-500/10 flex items-center justify-center text-zinc-600 shrink-0">
<Download className="h-6 w-6" /> <Download className="h-6 w-6" />
</div> </div>
<div> <div>

View File

@@ -29,9 +29,11 @@ export function GeneralSettingsClient({ initialSettings }: GeneralSettingsClient
await updateAISettings({ preferredLanguage: value as any }) await updateAISettings({ preferredLanguage: value as any })
if (value === 'auto') { if (value === 'auto') {
localStorage.removeItem('user-language') localStorage.removeItem('user-language')
document.cookie = 'user-language=;path=/;max-age=0'
toast.success(t('settings.languageAuto') || 'Language set to Auto') toast.success(t('settings.languageAuto') || 'Language set to Auto')
} else { } else {
localStorage.setItem('user-language', value) localStorage.setItem('user-language', value)
document.cookie = `user-language=${value};path=/;max-age=${60 * 60 * 24 * 365};samesite=lax`
setContextLanguage(value as any) setContextLanguage(value as any)
toast.success(t('profile.languageUpdateSuccess') || 'Language updated') toast.success(t('profile.languageUpdateSuccess') || 'Language updated')
} }

View File

@@ -9,7 +9,16 @@ export default async function GeneralSettingsPage() {
redirect('/api/auth/signin') redirect('/api/auth/signin')
} }
const settings = await getAISettings() const {
preferredLanguage,
emailNotifications,
desktopNotifications,
autoSave,
} = await getAISettings()
return <GeneralSettingsClient initialSettings={settings} /> return (
<GeneralSettingsClient
initialSettings={{ preferredLanguage, emailNotifications, desktopNotifications, autoSave }}
/>
)
} }

View File

@@ -273,22 +273,21 @@ Focus ONLY on this note unless asked otherwise.`
const provider = getChatProvider(sysConfig) const provider = getChatProvider(sysConfig)
const result = await streamText({ const result = await streamText({
model: provider.chatModel, model: provider.getModel(),
system: systemPrompt, system: systemPrompt,
messages: incomingMessages, messages: incomingMessages,
tools: chatTools, tools: chatTools,
maxSteps: 5, maxSteps: 5,
onFinish: async (final) => { onFinish: async (final) => {
// Save messages to DB
const userContent = incomingMessages[incomingMessages.length - 1].content const userContent = incomingMessages[incomingMessages.length - 1].content
await prisma.message.create({ await prisma.chatMessage.create({
data: { conversationId: conversation.id, role: 'user', content: userContent } data: { conversationId: conversation.id, role: 'user', content: userContent }
}) })
await prisma.message.create({ await prisma.chatMessage.create({
data: { conversationId: conversation.id, role: 'assistant', content: final.text } data: { conversationId: conversation.id, role: 'assistant', content: final.text }
}) })
} }
}) })
return result.toDataStreamResponse() return result.toUIMessageStreamResponse()
} }

View File

@@ -15,9 +15,11 @@
/* Memento — Architectural Grid (réf. architectural-grid1) */ /* Memento — Architectural Grid (réf. architectural-grid1) */
--color-memento-desk: #E5E2D9; --color-memento-desk: #E5E2D9;
--color-memento-paper: #F2F0E9; --color-memento-paper: #F2F0E9;
--color-memento-sidebar: #F6F4F0;
--color-memento-ink: #1C1C1C; --color-memento-ink: #1C1C1C;
--color-primary: #ACB995; --color-primary: #ACB995;
--color-memento-accent: #D4A373; --color-memento-accent: #D4A373;
--color-memento-blue: #75B2D6;
--color-memento-paper-elevated: #faf9f5; --color-memento-paper-elevated: #faf9f5;
--color-background-light: var(--color-memento-paper); --color-background-light: var(--color-memento-paper);
--color-background-dark: #202020; --color-background-dark: #202020;
@@ -117,12 +119,10 @@
/* Architectural Grid — texture & navigation (réf. architectural-grid1) */ /* Architectural Grid — texture & navigation (réf. architectural-grid1) */
.memento-paper-texture { .memento-paper-texture {
background-color: var(--background); background-color: var(--background);
background-image: url("https://www.transparenttextures.com/patterns/natural-paper.png");
background-size: auto;
} }
html.dark .memento-paper-texture { html.dark .memento-paper-texture {
background-image: none; background-color: var(--background);
} }
.memento-sidebar-depth { .memento-sidebar-depth {
@@ -344,8 +344,8 @@ html.dark .memento-active-nav {
:root { :root {
--radius: 0.5rem; --radius: 0.5rem;
--memento-desk: #F9F8F6; --memento-desk: #E5E2D9;
--background: #F9F8F6; --background: #F2F0E9;
--foreground: #212529; --foreground: #212529;
--card: #ffffff; --card: #ffffff;
--card-foreground: #212529; --card-foreground: #212529;
@@ -353,27 +353,27 @@ html.dark .memento-active-nav {
--popover-foreground: #212529; --popover-foreground: #212529;
--primary: #ACB995; --primary: #ACB995;
--primary-foreground: #F9F8F6; --primary-foreground: #F9F8F6;
--secondary: #75B2D6; --secondary: #E9ECEF;
--secondary-foreground: #212529; --secondary-foreground: #212529;
--muted: #75B2D6; --muted: #F1F3F5;
--muted-foreground: rgba(33, 37, 41, 0.6); --muted-foreground: rgba(33, 37, 41, 0.6);
--accent: #75B2D6; --accent: #F8F9FA;
--accent-foreground: #212529; --accent-foreground: #212529;
--destructive: #E11D48; --destructive: #E11D48;
--border: #75B2D6; --border: rgba(0, 0, 0, 0.08);
--input: #75B2D6; --input: rgba(0, 0, 0, 0.08);
--ring: rgba(33, 37, 41, 0.3); --ring: rgba(33, 37, 41, 0.3);
--ai-accent: #ACB995; --ai-accent: #ACB995;
--pinned-gold: #F59E0B; --pinned-gold: #F59E0B;
--sage-green: #10B981; --sage-green: #10B981;
--sidebar: #ffffff; --sidebar: #F6F4F0;
--sidebar-foreground: #212529; --sidebar-foreground: #212529;
--sidebar-primary: #212529; --sidebar-primary: #212529;
--sidebar-primary-foreground: #F9F8F6; --sidebar-primary-foreground: #F9F8F6;
--sidebar-accent: #F9F8F6; --sidebar-accent: #F9F8F6;
--sidebar-accent-foreground: #212529; --sidebar-accent-foreground: #212529;
--sidebar-border: #75B2D6; --sidebar-border: rgba(0, 0, 0, 0.05);
--sidebar-ring: rgba(33, 37, 41, 0.2); --sidebar-ring: rgba(33, 37, 41, 0.2);
} }
@@ -405,7 +405,7 @@ html.dark {
--border: rgba(28, 28, 28, 0.1); --border: rgba(28, 28, 28, 0.1);
--input: rgba(28, 28, 28, 0.12); --input: rgba(28, 28, 28, 0.12);
--ring: rgba(28, 28, 28, 0.35); --ring: rgba(28, 28, 28, 0.35);
--sidebar: color-mix(in oklab, #ffffff 65%, #F2F0E9); --sidebar: #F6F4F0;
--sidebar-foreground: #1C1C1C; --sidebar-foreground: #1C1C1C;
--sidebar-primary: #1C1C1C; --sidebar-primary: #1C1C1C;
--sidebar-primary-foreground: #F2F0E9; --sidebar-primary-foreground: #F2F0E9;
@@ -479,139 +479,177 @@ html.dark {
} }
[data-theme='midnight'] { [data-theme='midnight'] {
--background: oklch(0.94 0.005 250); --background: #F8F9FA;
/* Gris-bleu très pâle */ --foreground: #212529;
--foreground: oklch(0.18 0.03 250); --card: #ffffff;
/* Gris-bleu très foncé */ --card-foreground: #212529;
--card: oklch(0.97 0.006 250); --primary: #ACB995;
/* Gris-bleu pâle */ --primary-foreground: #F8F9FA;
--card-foreground: oklch(0.18 0.03 250); --secondary: #E9ECEF;
--primary: oklch(0.5 0.12 250); --secondary-foreground: #212529;
/* Gris-bleu saturé */ --muted: #F1F3F5;
--primary-foreground: oklch(0.99 0 0); --muted-foreground: rgba(33, 37, 41, 0.6);
/* Blanc */ --accent: #F8F9FA;
--secondary: oklch(0.2 0.01 250); --accent-foreground: #212529;
--secondary-foreground: oklch(0.18 0.03 250); --destructive: #E11D48;
--muted: oklch(0.22 0.01 250); --border: rgba(0, 0, 0, 0.08);
--muted-foreground: oklch(0.55 0.02 250); --input: rgba(0, 0, 0, 0.08);
--accent: oklch(0.25 0.015 250); --ring: rgba(33, 37, 41, 0.3);
--accent-foreground: oklch(0.18 0.03 250); --popover: #ffffff;
--destructive: oklch(0.6 0.22 25); --popover-foreground: #212529;
--border: oklch(0.82 0.015 250); --sidebar: #ffffff;
--input: oklch(0.82 0.015 250); --sidebar-foreground: #212529;
--ring: oklch(0.65 0.015 250); --sidebar-primary: #212529;
--popover: oklch(0.97 0.006 250); --sidebar-primary-foreground: #F8F9FA;
--popover-foreground: oklch(0.18 0.03 250); --sidebar-accent: #F8F9FA;
--sidebar: oklch(0.9 0.01 250); --sidebar-accent-foreground: #212529;
--sidebar-foreground: oklch(0.18 0.03 250); --sidebar-border: rgba(0, 0, 0, 0.05);
--sidebar-primary: oklch(0.5 0.12 250); --sidebar-ring: rgba(33, 37, 41, 0.2);
--sidebar-primary-foreground: oklch(0.99 0 0);
--sidebar-accent: oklch(0.25 0.015 250);
--sidebar-accent-foreground: oklch(0.18 0.03 250);
--sidebar-border: oklch(0.85 0.015 250);
--sidebar-ring: oklch(0.65 0.015 250);
} }
[data-theme='midnight'].dark { [data-theme='midnight'].dark {
--background: oklch(0.1 0.01 250); --background: #1C1C1C;
/* Noir profond */ --foreground: #F8F9FA;
--foreground: oklch(0.96 0.005 250); --card: #252525;
/* Blanc grisâtre */ --card-foreground: #F8F9FA;
--card: oklch(0.15 0.015 250); --primary: #ACB995;
/* Gris-bleu très foncé */ --primary-foreground: #1C1C1C;
--card-foreground: oklch(0.96 0.005 250); --secondary: #2A2A2A;
--primary: oklch(0.6 0.12 250); --secondary-foreground: #F8F9FA;
/* Gris-bleu vibrant */ --muted: #222222;
--primary-foreground: oklch(0.1 0 0); --muted-foreground: rgba(248, 249, 250, 0.5);
/* Noir */ --accent: #2A2A2A;
--secondary: oklch(0.18 0.015 250); --accent-foreground: #F8F9FA;
--secondary-foreground: oklch(0.96 0.005 250); --destructive: #E11D48;
--muted: oklch(0.2 0.015 250); --border: rgba(255, 255, 255, 0.1);
--muted-foreground: oklch(0.5 0.02 250); --input: rgba(255, 255, 255, 0.1);
--accent: oklch(0.26 0.02 250); --ring: rgba(255, 255, 255, 0.2);
--accent-foreground: oklch(0.96 0.005 250); --popover: #252525;
--destructive: oklch(0.65 0.2 25); --popover-foreground: #F8F9FA;
--border: oklch(0.33 0.02 250); --sidebar: #181818;
--input: oklch(0.33 0.02 250); --sidebar-foreground: #F8F9FA;
--ring: oklch(0.55 0.02 250); --sidebar-primary: #F8F9FA;
--popover: oklch(0.15 0.015 250); --sidebar-primary-foreground: #1C1C1C;
--popover-foreground: oklch(0.96 0.005 250); --sidebar-accent: #2A2A2A;
--sidebar: oklch(0.08 0.01 250); --sidebar-accent-foreground: #F8F9FA;
--sidebar-foreground: oklch(0.96 0.005 250); --sidebar-border: rgba(255, 255, 255, 0.08);
--sidebar-primary: oklch(0.6 0.12 250); --sidebar-ring: rgba(255, 255, 255, 0.15);
--sidebar-primary-foreground: oklch(0.1 0 0);
--sidebar-accent: oklch(0.22 0.02 250);
--sidebar-accent-foreground: oklch(0.96 0.005 250);
--sidebar-border: oklch(0.3 0.02 250);
--sidebar-ring: oklch(0.55 0.02 250);
} }
[data-theme='blue'] { [data-theme='blue'] {
--background: oklch(0.985 0.005 225); --background: #F8F9FA;
/* Blanc légèrement bleuté */ --foreground: #212529;
--foreground: oklch(0.18 0.035 225); --card: #ffffff;
/* Gris-bleu foncé saturé */ --card-foreground: #212529;
--card: oklch(1 0 0); --primary: #ACB995;
/* Blanc pur */ --primary-foreground: #F8F9FA;
--card-foreground: oklch(0.18 0.035 225); --secondary: #E9ECEF;
--primary: oklch(0.5 0.15 225); --secondary-foreground: #212529;
/* Bleu vibrant */ --muted: #F1F3F5;
--primary-foreground: oklch(0.99 0 0); --muted-foreground: rgba(33, 37, 41, 0.6);
/* Blanc */ --accent: #F8F9FA;
--secondary: oklch(0.93 0.008 225); --accent-foreground: #212529;
--secondary-foreground: oklch(0.18 0.035 225); --destructive: #E11D48;
--muted: oklch(0.9 0.01 225); --border: rgba(0, 0, 0, 0.08);
--muted-foreground: oklch(0.58 0.015 225); --input: rgba(0, 0, 0, 0.08);
--accent: oklch(0.93 0.01 225); --ring: rgba(33, 37, 41, 0.3);
--accent-foreground: oklch(0.18 0.035 225); --popover: #ffffff;
--destructive: oklch(0.6 0.2 25); --popover-foreground: #212529;
--border: oklch(0.83 0.012 225); --sidebar: #ffffff;
--input: oklch(0.83 0.012 225); --sidebar-foreground: #212529;
--ring: oklch(0.65 0.015 225); --sidebar-primary: #212529;
--popover: oklch(1 0 0); --sidebar-primary-foreground: #F8F9FA;
--popover-foreground: oklch(0.18 0.035 225); --sidebar-accent: #F8F9FA;
--sidebar: oklch(0.965 0.008 225); --sidebar-accent-foreground: #212529;
--sidebar-foreground: oklch(0.18 0.035 225); --sidebar-border: rgba(0, 0, 0, 0.05);
--sidebar-primary: oklch(0.5 0.15 225); --sidebar-ring: rgba(33, 37, 41, 0.2);
--sidebar-primary-foreground: oklch(0.99 0 0);
--sidebar-accent: oklch(0.93 0.01 225);
--sidebar-accent-foreground: oklch(0.18 0.035 225);
--sidebar-border: oklch(0.87 0.012 225);
--sidebar-ring: oklch(0.65 0.015 225);
} }
[data-theme='blue'].dark { [data-theme='blue'].dark {
--background: oklch(0.13 0.008 225); --background: #1C1C1C;
/* Noir légèrement bleuté */ --foreground: #F8F9FA;
--foreground: oklch(0.97 0.006 225); --card: #252525;
/* Blanc légèrement bleuté */ --card-foreground: #F8F9FA;
--card: oklch(0.17 0.01 225); --primary: #ACB995;
/* Gris-bleu foncé */ --primary-foreground: #1C1C1C;
--card-foreground: oklch(0.97 0.006 225); --secondary: #2A2A2A;
--primary: oklch(0.6 0.15 225); --secondary-foreground: #F8F9FA;
/* Bleu vibrant plus clair */ --muted: #222222;
--primary-foreground: oklch(0.1 0 0); --muted-foreground: rgba(248, 249, 250, 0.5);
/* Noir */ --accent: #2A2A2A;
--secondary: oklch(0.22 0.015 225); --accent-foreground: #F8F9FA;
--secondary-foreground: oklch(0.97 0.006 225); --destructive: #E11D48;
--muted: oklch(0.25 0.02 225); --border: rgba(255, 255, 255, 0.1);
--muted-foreground: oklch(0.52 0.018 225); --input: rgba(255, 255, 255, 0.1);
--accent: oklch(0.28 0.025 225); --ring: rgba(255, 255, 255, 0.2);
--accent-foreground: oklch(0.97 0.006 225); --popover: #252525;
--destructive: oklch(0.65 0.22 25); --popover-foreground: #F8F9FA;
--border: oklch(0.35 0.018 225); --sidebar: #181818;
--input: oklch(0.35 0.018 225); --sidebar-foreground: #F8F9FA;
--ring: oklch(0.55 0.02 225); --sidebar-primary: #F8F9FA;
--popover: oklch(0.17 0.01 225); --sidebar-primary-foreground: #1C1C1C;
--popover-foreground: oklch(0.97 0.006 225); --sidebar-accent: #2A2A2A;
--sidebar: oklch(0.1 0.01 225); --sidebar-accent-foreground: #F8F9FA;
--sidebar-foreground: oklch(0.97 0.006 225); --sidebar-border: rgba(255, 255, 255, 0.08);
--sidebar-primary: oklch(0.6 0.15 225); --sidebar-ring: rgba(255, 255, 255, 0.15);
--sidebar-primary-foreground: oklch(0.1 0 0); }
--sidebar-accent: oklch(0.25 0.025 225);
--sidebar-accent-foreground: oklch(0.97 0.006 225); [data-theme='ocean'] {
--sidebar-border: oklch(0.32 0.018 225); --background: #F8F9FA;
--sidebar-ring: oklch(0.55 0.02 225); --foreground: #212529;
--card: #ffffff;
--card-foreground: #212529;
--primary: #ACB995;
--primary-foreground: #F8F9FA;
--secondary: #E9ECEF;
--secondary-foreground: #212529;
--muted: #F1F3F5;
--muted-foreground: rgba(33, 37, 41, 0.6);
--accent: #F8F9FA;
--accent-foreground: #212529;
--destructive: #E11D48;
--border: rgba(0, 0, 0, 0.08);
--input: rgba(0, 0, 0, 0.08);
--ring: rgba(33, 37, 41, 0.3);
--popover: #ffffff;
--popover-foreground: #212529;
--sidebar: #ffffff;
--sidebar-foreground: #212529;
--sidebar-primary: #212529;
--sidebar-primary-foreground: #F8F9FA;
--sidebar-accent: #F8F9FA;
--sidebar-accent-foreground: #212529;
--sidebar-border: rgba(0, 0, 0, 0.05);
--sidebar-ring: rgba(33, 37, 41, 0.2);
}
[data-theme='ocean'].dark {
--background: #1C1C1C;
--foreground: #F8F9FA;
--card: #252525;
--card-foreground: #F8F9FA;
--primary: #ACB995;
--primary-foreground: #1C1C1C;
--secondary: #2A2A2A;
--secondary-foreground: #F8F9FA;
--muted: #222222;
--muted-foreground: rgba(248, 249, 250, 0.5);
--accent: #2A2A2A;
--accent-foreground: #F8F9FA;
--destructive: #E11D48;
--border: rgba(255, 255, 255, 0.1);
--input: rgba(255, 255, 255, 0.1);
--ring: rgba(255, 255, 255, 0.2);
--popover: #252525;
--popover-foreground: #F8F9FA;
--sidebar: #181818;
--sidebar-foreground: #F8F9FA;
--sidebar-primary: #F8F9FA;
--sidebar-primary-foreground: #1C1C1C;
--sidebar-accent: #2A2A2A;
--sidebar-accent-foreground: #F8F9FA;
--sidebar-border: rgba(255, 255, 255, 0.08);
--sidebar-ring: rgba(255, 255, 255, 0.15);
} }
[data-theme='sepia'] { [data-theme='sepia'] {
@@ -922,64 +960,7 @@ html.dark {
--sidebar-ring: oklch(0.6 0.02 55); --sidebar-ring: oklch(0.6 0.02 55);
} }
/* Ocean — soft teal/cyan tones */
[data-theme='ocean'] {
--background: oklch(0.97 0.01 195);
--foreground: oklch(0.22 0.02 195);
--card: oklch(0.99 0.005 195);
--card-foreground: oklch(0.22 0.02 195);
--popover: oklch(0.99 0.005 195);
--popover-foreground: oklch(0.22 0.02 195);
--primary: oklch(0.5 0.12 195);
--primary-foreground: oklch(0.99 0 0);
--secondary: oklch(0.93 0.015 195);
--secondary-foreground: oklch(0.22 0.02 195);
--muted: oklch(0.92 0.012 195);
--muted-foreground: oklch(0.55 0.02 195);
--accent: oklch(0.92 0.015 195);
--accent-foreground: oklch(0.22 0.02 195);
--destructive: oklch(0.6 0.18 25);
--border: oklch(0.85 0.015 195);
--input: oklch(0.85 0.015 195);
--ring: oklch(0.5 0.12 195);
--sidebar: oklch(0.95 0.012 195);
--sidebar-foreground: oklch(0.22 0.02 195);
--sidebar-primary: oklch(0.5 0.12 195);
--sidebar-primary-foreground: oklch(0.99 0 0);
--sidebar-accent: oklch(0.92 0.015 195);
--sidebar-accent-foreground: oklch(0.22 0.02 195);
--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);
--card: oklch(0.18 0.015 195);
--card-foreground: oklch(0.95 0.01 195);
--popover: oklch(0.18 0.015 195);
--popover-foreground: oklch(0.95 0.01 195);
--primary: oklch(0.6 0.1 195);
--primary-foreground: oklch(0.1 0 0);
--secondary: oklch(0.24 0.02 195);
--secondary-foreground: oklch(0.95 0.01 195);
--muted: oklch(0.22 0.015 195);
--muted-foreground: oklch(0.55 0.02 195);
--accent: oklch(0.26 0.02 195);
--accent-foreground: oklch(0.95 0.01 195);
--destructive: oklch(0.65 0.18 25);
--border: oklch(0.33 0.02 195);
--input: oklch(0.33 0.02 195);
--ring: oklch(0.6 0.02 195);
--sidebar: oklch(0.12 0.01 195);
--sidebar-foreground: oklch(0.95 0.01 195);
--sidebar-primary: oklch(0.6 0.1 195);
--sidebar-primary-foreground: oklch(0.1 0 0);
--sidebar-accent: oklch(0.26 0.02 195);
--sidebar-accent-foreground: oklch(0.95 0.01 195);
--sidebar-border: oklch(0.33 0.02 195);
--sidebar-ring: oklch(0.6 0.02 195);
}
/* Sunset — warm coral/peach tones */ /* Sunset — warm coral/peach tones */
[data-theme='sunset'] { [data-theme='sunset'] {

View File

@@ -75,6 +75,10 @@ const directionScript = `
(function(){ (function(){
try { try {
var lang = localStorage.getItem('user-language'); var lang = localStorage.getItem('user-language');
if (!lang) {
var c = document.cookie.split(';').map(function(s){return s.trim()}).find(function(s){return s.startsWith('user-language=')});
if (c) lang = c.split('=')[1];
}
if (lang === 'fa' || lang === 'ar') { if (lang === 'fa' || lang === 'ar') {
document.documentElement.dir = 'rtl'; document.documentElement.dir = 'rtl';
document.documentElement.lang = lang; document.documentElement.lang = lang;

View File

@@ -1,6 +1,8 @@
'use client' 'use client'
import { LanguageProvider } from '@/lib/i18n/LanguageProvider' import { LanguageProvider } from '@/lib/i18n/LanguageProvider'
import { NoteRefreshProvider } from '@/context/NoteRefreshContext'
import { QueryProvider } from '@/components/query-provider'
import type { Translations } from '@/lib/i18n/load-translations' import type { Translations } from '@/lib/i18n/load-translations'
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
@@ -16,11 +18,15 @@ export function AdminProvidersWrapper({
initialTranslations, initialTranslations,
}: AdminProvidersWrapperProps) { }: AdminProvidersWrapperProps) {
return ( return (
<QueryProvider>
<NoteRefreshProvider>
<LanguageProvider <LanguageProvider
initialLanguage={initialLanguage as any} initialLanguage={initialLanguage as any}
initialTranslations={initialTranslations} initialTranslations={initialTranslations}
> >
{children} {children}
</LanguageProvider> </LanguageProvider>
</NoteRefreshProvider>
</QueryProvider>
) )
} }

View File

@@ -83,6 +83,7 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
} }
} }
try {
await sendMessage( await sendMessage(
{ text }, { text },
{ {
@@ -96,6 +97,10 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
} }
} }
) )
} catch (error) {
console.error('Chat send error:', error)
toast.error(t('chat.assistantError') || 'Failed to send message')
}
} }
const fetchHistory = async () => { const fetchHistory = async () => {
@@ -147,18 +152,18 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
return ( return (
<Button <Button
onClick={() => setIsOpen(true)} onClick={() => setIsOpen(true)}
className="fixed bottom-6 right-6 h-12 w-12 rounded-full shadow-xl z-40 transition-transform hover:scale-105 bg-[#E9ECEF] text-[#1C1C1C] hover:bg-[#E9ECEF]/80 border border-black/5" className="fixed bottom-6 right-6 h-12 w-12 rounded-full shadow-xl z-40 transition-transform hover:scale-105 bg-muted text-foreground hover:bg-muted/80 border border-border"
size="icon" size="icon"
title={t('ai.openAssistant')} title={t('ai.openAssistant')}
> >
<Sparkles className="h-5 w-5" /> <Sparkles className="h-5 w-5 text-memento-accent" />
</Button> </Button>
) )
} }
return ( return (
<aside className={cn( <aside className={cn(
"fixed bottom-20 right-6 border border-border/40 bg-card flex flex-col z-40 shadow-2xl rounded-2xl overflow-hidden transition-all duration-300", "fixed bottom-20 right-6 border border-border/40 bg-background flex flex-col z-40 shadow-2xl rounded-2xl overflow-hidden transition-all duration-300",
isExpanded ? "w-[80vw] h-[85vh] max-w-[1200px]" : "h-[700px] max-h-[85vh] w-[360px]" isExpanded ? "w-[80vw] h-[85vh] max-w-[1200px]" : "h-[700px] max-h-[85vh] w-[360px]"
)}> )}>
{/* Header */} {/* Header */}
@@ -202,7 +207,7 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
onClick={() => setActiveTab('chat')} onClick={() => setActiveTab('chat')}
className={cn( className={cn(
"flex-1 pb-3 border-b-2 text-sm font-semibold flex items-center justify-center gap-2 transition-all", "flex-1 pb-3 border-b-2 text-sm font-semibold flex items-center justify-center gap-2 transition-all",
activeTab === 'chat' ? "border-[#75B2D6] text-[#75B2D6]" : "border-transparent text-muted-foreground hover:text-foreground" activeTab === 'chat' ? "border-memento-blue text-memento-blue" : "border-transparent text-muted-foreground hover:text-foreground"
)} )}
> >
<Bot className="h-4 w-4" /> {t('ai.chatTab')} <Bot className="h-4 w-4" /> {t('ai.chatTab')}
@@ -211,7 +216,7 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
onClick={() => setActiveTab('insights')} onClick={() => setActiveTab('insights')}
className={cn( className={cn(
"flex-1 pb-3 border-b-2 text-sm font-semibold flex items-center justify-center gap-2 transition-all", "flex-1 pb-3 border-b-2 text-sm font-semibold flex items-center justify-center gap-2 transition-all",
activeTab === 'insights' ? "border-[#75B2D6] text-[#75B2D6]" : "border-transparent text-muted-foreground hover:text-foreground" activeTab === 'insights' ? "border-memento-blue text-memento-blue" : "border-transparent text-muted-foreground hover:text-foreground"
)} )}
> >
<Sparkles className="h-4 w-4" /> {t('ai.insightsTab')} <Sparkles className="h-4 w-4" /> {t('ai.insightsTab')}
@@ -220,7 +225,7 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
onClick={() => setActiveTab('history')} onClick={() => setActiveTab('history')}
className={cn( className={cn(
"flex-1 pb-3 border-b-2 text-sm font-semibold flex items-center justify-center gap-2 transition-all", "flex-1 pb-3 border-b-2 text-sm font-semibold flex items-center justify-center gap-2 transition-all",
activeTab === 'history' ? "border-[#75B2D6] text-[#75B2D6]" : "border-transparent text-muted-foreground hover:text-foreground" activeTab === 'history' ? "border-memento-blue text-memento-blue" : "border-transparent text-muted-foreground hover:text-foreground"
)} )}
> >
<History className="h-4 w-4" /> {t('ai.historyTab')} <History className="h-4 w-4" /> {t('ai.historyTab')}
@@ -234,10 +239,10 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
{/* AI Welcome Message */} {/* AI Welcome Message */}
{messages.length === 0 && ( {messages.length === 0 && (
<div className="flex gap-3"> <div className="flex gap-3">
<div className="w-8 h-8 rounded-full bg-[#75B2D6]/10 text-[#75B2D6] flex items-center justify-center flex-shrink-0 border border-[#75B2D6]/20"> <div className="w-8 h-8 rounded-full bg-memento-blue/10 text-memento-blue flex items-center justify-center flex-shrink-0 border border-memento-blue/20">
<Bot className="h-4 w-4" /> <Bot className="h-4 w-4" />
</div> </div>
<div className="bg-[#FDFDFE] border border-border/50 p-3.5 rounded-2xl rounded-tl-sm shadow-sm"> <div className="bg-background border border-border/50 p-3.5 rounded-2xl rounded-tl-sm shadow-sm">
<p className="text-sm text-foreground leading-relaxed"> <p className="text-sm text-foreground leading-relaxed">
{t('ai.welcomeMsg')} {t('ai.welcomeMsg')}
</p> </p>
@@ -255,16 +260,16 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
<div className={cn( <div className={cn(
'w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 border text-[10px] font-bold', 'w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 border text-[10px] font-bold',
msg.role === 'user' msg.role === 'user'
? 'bg-slate-100 dark:bg-slate-800 border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-300' ? 'bg-muted border-border text-muted-foreground'
: 'bg-[#75B2D6]/10 text-[#75B2D6] border-[#75B2D6]/20', : 'bg-memento-blue/10 text-memento-blue border-memento-blue/20',
)}> )}>
{msg.role === 'user' ? 'U' : <Bot className="h-4 w-4" />} {msg.role === 'user' ? 'U' : <Bot className="h-4 w-4" />}
</div> </div>
<div className={cn( <div className={cn(
'max-w-[85%] p-3.5 rounded-2xl text-sm leading-relaxed shadow-sm', 'max-w-[85%] p-3.5 rounded-2xl text-sm leading-relaxed shadow-sm',
msg.role === 'user' msg.role === 'user'
? 'bg-[#75B2D6] text-white rounded-tr-sm' ? 'bg-memento-blue text-white rounded-tr-sm'
: 'bg-[#FDFDFE] border border-border/50 rounded-tl-sm text-foreground', : 'bg-background border border-border/50 rounded-tl-sm text-foreground',
)}> )}>
{msg.role === 'assistant' {msg.role === 'assistant'
? <MarkdownContent content={text} /> ? <MarkdownContent content={text} />
@@ -276,10 +281,10 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
{isLoading && ( {isLoading && (
<div className="flex gap-3"> <div className="flex gap-3">
<div className="w-8 h-8 rounded-full bg-[#75B2D6]/10 text-[#75B2D6] flex items-center justify-center flex-shrink-0 border border-[#75B2D6]/20"> <div className="w-8 h-8 rounded-full bg-memento-blue/10 text-memento-blue flex items-center justify-center flex-shrink-0 border border-memento-blue/20">
<Bot className="h-4 w-4" /> <Bot className="h-4 w-4" />
</div> </div>
<div className="bg-[#FDFDFE] border border-border/50 p-3.5 rounded-2xl rounded-tl-sm shadow-sm"> <div className="bg-background border border-border/50 p-3.5 rounded-2xl rounded-tl-sm shadow-sm">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" /> <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</div> </div>
</div> </div>
@@ -290,7 +295,7 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
{activeTab === 'insights' && ( {activeTab === 'insights' && (
<div className="h-full"> <div className="h-full">
<h3 className="text-sm font-semibold mb-4 flex items-center gap-2"><Sparkles className="h-4 w-4 text-[#75B2D6]" /> {t('ai.summaryLast5')}</h3> <h3 className="text-sm font-semibold mb-4 flex items-center gap-2"><Sparkles className="h-4 w-4 text-memento-accent" /> {t('ai.summaryLast5')}</h3>
{insightsLoading ? ( {insightsLoading ? (
<div className="flex flex-col items-center justify-center py-10 opacity-60"> <div className="flex flex-col items-center justify-center py-10 opacity-60">
<Loader2 className="h-8 w-8 animate-spin mb-4 text-muted-foreground" /> <Loader2 className="h-8 w-8 animate-spin mb-4 text-muted-foreground" />
@@ -318,7 +323,7 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
history.map(conv => ( history.map(conv => (
<button <button
key={conv.id} key={conv.id}
className="w-full text-left p-3 rounded-xl border border-border/50 hover:bg-muted/50 hover:border-[#75B2D6]/30 transition-all flex flex-col gap-1" className="w-full text-left p-3 rounded-xl border border-border/50 hover:bg-muted/50 hover:border-memento-blue/30 transition-all flex flex-col gap-1"
onClick={() => { onClick={() => {
setConversationId(conv.id) setConversationId(conv.id)
setMessages(conv.messages.map((m: any) => ({ setMessages(conv.messages.map((m: any) => ({
@@ -345,12 +350,12 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
</div> </div>
{/* Input Area & Tone Controls (Only in Chat tab) */} {/* Input Area & Tone Controls (Only in Chat tab) */}
<div className={cn("p-4 border-t border-border/40 bg-[#FDFDFE] shrink-0", activeTab !== 'chat' && "hidden")}> <div className={cn("p-4 border-t border-border/40 bg-background shrink-0", activeTab !== 'chat' && "hidden")}>
{/* Context Scope */} {/* Context Scope */}
<div className="mb-3"> <div className="mb-3">
<span className="text-[9px] font-bold uppercase tracking-widest text-muted-foreground block mb-1.5 ml-1">{t('ai.discussionContextLabel')}</span> <span className="text-[9px] font-bold uppercase tracking-widest text-muted-foreground block mb-1.5 ml-1">{t('ai.discussionContextLabel')}</span>
<Select value={chatScope} onValueChange={setChatScope}> <Select value={chatScope} onValueChange={setChatScope}>
<SelectTrigger className="h-8 text-xs bg-card border-border/60"> <SelectTrigger className="h-8 text-xs bg-background border-border/60">
<SelectValue placeholder={t('ai.selectNotebook')} /> <SelectValue placeholder={t('ai.selectNotebook')} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -387,8 +392,8 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
className={cn( className={cn(
"py-1 rounded-md border text-[10px] font-medium transition-all flex flex-col items-center justify-center gap-0.5", "py-1 rounded-md border text-[10px] font-medium transition-all flex flex-col items-center justify-center gap-0.5",
isSelected isSelected
? "border-[#75B2D6] bg-[#75B2D6]/10 text-[#75B2D6] shadow-sm" ? "border-memento-blue bg-memento-blue/10 text-memento-blue shadow-sm"
: "border-border/60 bg-card text-muted-foreground hover:bg-muted hover:border-border" : "border-border/60 bg-background text-muted-foreground hover:bg-muted hover:border-border"
)} )}
> >
<Icon className="h-3 w-3" /> <Icon className="h-3 w-3" />
@@ -400,7 +405,7 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
</div> </div>
{/* Text Input */} {/* Text Input */}
<div className="relative bg-card border border-border/60 rounded-xl p-1 focus-within:border-[#75B2D6] focus-within:ring-1 focus-within:ring-[#75B2D6]/20 transition-all shadow-sm"> <div className="relative bg-background border border-border/60 rounded-xl p-1 focus-within:border-memento-blue focus-within:ring-1 focus-within:ring-memento-blue/20 transition-all shadow-sm">
<textarea <textarea
className="w-full bg-transparent border-none focus:ring-0 resize-none text-sm text-foreground placeholder:text-muted-foreground/70 p-2 min-h-[60px] max-h-[120px]" className="w-full bg-transparent border-none focus:ring-0 resize-none text-sm text-foreground placeholder:text-muted-foreground/70 p-2 min-h-[60px] max-h-[120px]"
placeholder={t('ai.chatPlaceholder')} placeholder={t('ai.chatPlaceholder')}
@@ -435,7 +440,7 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
) : ( ) : (
<Button <Button
size="icon" size="icon"
className="h-8 w-8 rounded-lg bg-[#75B2D6] text-white shadow-sm hover:shadow-md transition-all" className="h-8 w-8 rounded-lg bg-memento-blue text-white shadow-sm hover:shadow-md transition-all"
onClick={handleSend} onClick={handleSend}
disabled={!input.trim()} disabled={!input.trim()}
> >

View File

@@ -137,6 +137,7 @@ export function ChatContainer({ initialConversations, notebooks, webSearchAvaila
} }
} }
try {
await sendMessage( await sendMessage(
{ text: content }, { text: content },
{ {
@@ -148,6 +149,10 @@ export function ChatContainer({ initialConversations, notebooks, webSearchAvaila
}, },
} }
) )
} catch (error) {
console.error('Chat send error:', error)
toast.error(t('chat.assistantError') || 'Failed to send message')
}
} }
const handleNewChat = () => { const handleNewChat = () => {
@@ -183,7 +188,7 @@ export function ChatContainer({ initialConversations, notebooks, webSearchAvaila
}, [displayMessages]) }, [displayMessages])
return ( return (
<div className="flex-1 flex overflow-hidden bg-white dark:bg-[#1a1c22]"> <div className="flex-1 flex overflow-hidden bg-background">
<ChatSidebar <ChatSidebar
conversations={conversations} conversations={conversations}
currentId={currentId} currentId={currentId}
@@ -197,7 +202,7 @@ export function ChatContainer({ initialConversations, notebooks, webSearchAvaila
<ChatMessages messages={displayMessages} isLoading={isLoading || isLoadingHistory} /> <ChatMessages messages={displayMessages} isLoading={isLoading || isLoadingHistory} />
</div> </div>
<div className="w-full flex justify-center sticky bottom-0 bg-gradient-to-t from-white dark:from-[#1a1c22] via-white/90 dark:via-[#1a1c22]/90 to-transparent pt-6 pb-4"> <div className="w-full flex justify-center sticky bottom-0 bg-gradient-to-t from-background via-background/90 to-transparent pt-6 pb-4">
<div className="w-full max-w-4xl px-4"> <div className="w-full max-w-4xl px-4">
<ChatInput <ChatInput
onSend={handleSendMessage} onSend={handleSendMessage}

View File

@@ -64,7 +64,7 @@ export function ChatInput({ onSend, isLoading, onStop, notebooks, currentNoteboo
return ( return (
<div className="w-full relative"> <div className="w-full relative">
<div className="relative flex flex-col bg-slate-50 dark:bg-[#202228] rounded-[24px] border border-slate-200/60 dark:border-white/10 shadow-sm focus-within:shadow-md focus-within:border-slate-300 dark:focus-within:border-white/20 transition-all duration-300 overflow-hidden"> <div className="relative flex flex-col bg-muted/50 rounded-[24px] border border-border/60 shadow-sm focus-within:shadow-md focus-within:border-border transition-all duration-300 overflow-hidden">
{/* Input Area */} {/* Input Area */}
<Textarea <Textarea
@@ -84,7 +84,7 @@ export function ChatInput({ onSend, isLoading, onStop, notebooks, currentNoteboo
value={selectedNotebook || 'global'} value={selectedNotebook || 'global'}
onValueChange={(val) => setSelectedNotebook(val === 'global' ? undefined : val)} onValueChange={(val) => setSelectedNotebook(val === 'global' ? undefined : val)}
> >
<SelectTrigger className="h-8 w-auto min-w-[130px] rounded-full bg-white dark:bg-[#1a1c22] border-slate-200 dark:border-white/10 shadow-sm text-xs font-medium gap-2 ring-offset-transparent focus:ring-0 focus:ring-offset-0 hover:bg-slate-50 dark:hover:bg-[#252830] transition-colors"> <SelectTrigger className="h-8 w-auto min-w-[130px] rounded-full bg-background border-border/60 shadow-sm text-xs font-medium gap-2 ring-offset-transparent focus:ring-0 focus:ring-offset-0 hover:bg-muted/50 transition-colors">
<BookOpen className="h-3.5 w-3.5 text-muted-foreground" /> <BookOpen className="h-3.5 w-3.5 text-muted-foreground" />
<SelectValue placeholder={t('chat.allNotebooks')} /> <SelectValue placeholder={t('chat.allNotebooks')} />
</SelectTrigger> </SelectTrigger>
@@ -115,7 +115,7 @@ export function ChatInput({ onSend, isLoading, onStop, notebooks, currentNoteboo
"h-8 rounded-full border shadow-sm text-xs font-medium gap-1.5 flex items-center px-3 transition-all duration-200", "h-8 rounded-full border shadow-sm text-xs font-medium gap-1.5 flex items-center px-3 transition-all duration-200",
webSearchEnabled webSearchEnabled
? "bg-primary/10 text-primary border-primary/30 hover:bg-primary/20" ? "bg-primary/10 text-primary border-primary/30 hover:bg-primary/20"
: "bg-white dark:bg-[#1a1c22] border-slate-200 dark:border-white/10 text-slate-500 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-[#252830]" : "bg-background border-border/60 text-muted-foreground hover:bg-muted/50"
)} )}
> >
<Globe className="h-3.5 w-3.5" /> <Globe className="h-3.5 w-3.5" />

View File

@@ -52,7 +52,7 @@ export function ChatMessages({ messages, isLoading }: ChatMessagesProps) {
)} )}
> >
{msg.role === 'user' ? ( {msg.role === 'user' ? (
<div dir="auto" className="max-w-[85%] md:max-w-[70%] bg-[#f4f4f5] dark:bg-[#2a2d36] text-slate-800 dark:text-slate-100 rounded-3xl rounded-br-md px-6 py-4 shadow-sm border border-slate-200/50 dark:border-white/5"> <div dir="auto" className="max-w-[85%] md:max-w-[70%] bg-muted text-foreground rounded-3xl rounded-br-md px-6 py-4 shadow-sm border border-border/50">
<div className="prose prose-sm dark:prose-invert max-w-none text-[15px] leading-relaxed"> <div className="prose prose-sm dark:prose-invert max-w-none text-[15px] leading-relaxed">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown> <ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
</div> </div>

View File

@@ -46,7 +46,7 @@ export function ChatSidebar({
} }
return ( return (
<div className="w-64 border-r flex flex-col h-full bg-white dark:bg-[#1e2128]"> <div className="w-64 border-r flex flex-col h-full bg-sidebar">
<div className="p-4 border-bottom"> <div className="p-4 border-bottom">
<Button <Button
onClick={onNew} onClick={onNew}

View File

@@ -139,7 +139,7 @@ export function ContextualAIChat({
const { t, language } = useLanguage() const { t, language } = useLanguage()
const webSearchAvailable = useWebSearchAvailable() const webSearchAvailable = useWebSearchAvailable()
const [activeTab, setActiveTab] = useState<'chat' | 'actions' | 'resource'>('chat') const [activeTab, setActiveTab] = useState<'chat' | 'actions' | 'resource'>('actions')
const [selectedTone, setSelectedTone] = useState('professional') const [selectedTone, setSelectedTone] = useState('professional')
const [input, setInput] = useState('') const [input, setInput] = useState('')
const [chatScope, setChatScope] = useState<'note' | 'all' | string>('note') const [chatScope, setChatScope] = useState<'note' | 'all' | string>('note')
@@ -220,7 +220,12 @@ export function ContextualAIChat({
const text = input.trim() const text = input.trim()
if (!text || isLoading) return if (!text || isLoading) return
setInput('') setInput('')
try {
await sendMessage({ text }, { body: buildChatBody() }) await sendMessage({ text }, { body: buildChatBody() })
} catch (error) {
console.error('Chat send error:', error)
toast.error(t('chat.assistantError') || 'Failed to send message')
}
} }
// ── Action execution ──────────────────────────────────────────────────────── // ── Action execution ────────────────────────────────────────────────────────
@@ -477,12 +482,12 @@ export function ContextualAIChat({
<> <>
{expanded && ( {expanded && (
<div <div
className="fixed inset-0 z-[199] bg-[#1C1C1C]/40 backdrop-blur-sm" className="fixed inset-0 z-[199] bg-foreground/40 backdrop-blur-sm"
onClick={() => setExpanded(false)} onClick={() => setExpanded(false)}
/> />
)} )}
<aside className={cn( <aside className={cn(
'border-l border-border bg-[#F9F8F6] flex flex-col flex-shrink-0 z-10 transition-all duration-300 shadow-2xl', 'border-l border-border bg-background flex flex-col flex-shrink-0 z-10 transition-all duration-300 shadow-2xl',
expanded expanded
? 'fixed right-0 top-0 h-screen w-[640px] z-[200]' ? 'fixed right-0 top-0 h-screen w-[640px] z-[200]'
: 'h-full w-[360px]', : 'h-full w-[360px]',
@@ -492,27 +497,30 @@ export function ContextualAIChat({
<div className="p-6 border-b border-border shrink-0"> <div className="p-6 border-b border-border shrink-0">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="min-w-0 space-y-2"> <div className="min-w-0 space-y-2">
<h2 className="font-serif text-xl font-medium text-[#D4A373] flex items-center gap-2 leading-tight"> <h2 className="font-serif text-xl font-medium text-foreground flex items-center gap-2 leading-tight">
<Sparkles className="h-[18px] w-[18px] shrink-0 text-[#D4A373]" /> <Sparkles className="h-[18px] w-[18px] shrink-0 text-memento-accent" />
IA Assistant IA Assistant
</h2> </h2>
<p className="text-[11px] text-[#1C1C1C]/60 uppercase tracking-wider font-medium opacity-60 truncate"> <p className="text-[11px] text-foreground/60 uppercase tracking-wider font-medium opacity-60 truncate">
"{noteTitle || t('ai.currentNote')}" "{noteTitle || t('ai.currentNote')}"
</p> </p>
</div> </div>
<div className="flex items-center gap-1 shrink-0 -mt-1"> <div className="flex items-center gap-1 shrink-0 -mt-1">
<button <button
onClick={() => setExpanded(e => !e)} onClick={() => setExpanded(e => !e)}
className="p-1.5 hover:bg-white/40 rounded-full transition-colors text-[#1C1C1C]/40" className="p-1.5 hover:bg-muted/60 rounded-full transition-colors text-foreground/40"
title={expanded ? t('ai.shrinkPanel') : t('ai.expandPanel')} title={expanded ? t('ai.shrinkPanel') : t('ai.expandPanel')}
> >
{expanded ? <Minimize2 size={18} /> : <Maximize2 size={18} />} {expanded ? <Minimize2 size={18} /> : <Maximize2 size={18} />}
</button> </button>
<button <button
onClick={onClose} onClick={onClose}
className="p-1.5 hover:bg-white/40 rounded-full transition-colors text-[#1C1C1C]/40" className="p-1.5 hover:bg-muted/60 rounded-full transition-colors text-foreground/40 group"
> >
<ChevronRight size={20} /> <div className="relative w-5 h-5 flex items-center justify-center">
<ChevronRight size={20} className="transition-all duration-200 group-hover:opacity-0 group-hover:scale-0" />
<X size={18} className="absolute inset-0 m-auto opacity-0 scale-0 transition-all duration-200 group-hover:opacity-100 group-hover:scale-100" />
</div>
</button> </button>
</div> </div>
</div> </div>
@@ -520,8 +528,8 @@ export function ContextualAIChat({
<div className="flex border-b border-border shrink-0 px-2"> <div className="flex border-b border-border shrink-0 px-2">
{[ {[
{ id: 'chat', label: 'Discussion', icon: <MessageSquare size={16} /> },
{ id: 'actions', label: 'Actions', icon: <Sparkles size={16} /> }, { id: 'actions', label: 'Actions', icon: <Sparkles size={16} /> },
{ id: 'chat', label: 'Discussion', icon: <MessageSquare size={16} /> },
{ id: 'resource', label: 'Ressource', icon: <Link2 size={16} /> }, { id: 'resource', label: 'Ressource', icon: <Link2 size={16} /> },
].map(tab => ( ].map(tab => (
<button <button
@@ -529,12 +537,12 @@ export function ContextualAIChat({
onClick={() => setActiveTab(tab.id as any)} onClick={() => setActiveTab(tab.id as any)}
className={cn( className={cn(
"flex-1 flex items-center justify-center gap-2 py-3.5 text-[10px] font-bold uppercase tracking-[0.2em] transition-all relative", "flex-1 flex items-center justify-center gap-2 py-3.5 text-[10px] font-bold uppercase tracking-[0.2em] transition-all relative",
activeTab === tab.id ? 'text-[#1C1C1C]' : 'text-[#1C1C1C]/60 hover:text-[#1C1C1C]' activeTab === tab.id ? 'text-foreground' : 'text-foreground/60 hover:text-foreground'
)} )}
> >
{tab.label} {tab.label}
{activeTab === tab.id && ( {activeTab === tab.id && (
<motion.div layoutId="activeTab" className="absolute bottom-0 left-0 right-0 h-[2px] bg-[#D4A373]" /> <motion.div layoutId="activeTab" className="absolute bottom-0 left-0 right-0 h-[2px] bg-memento-blue" />
)} )}
</button> </button>
))} ))}
@@ -542,41 +550,41 @@ export function ContextualAIChat({
<div className="flex-1 flex flex-col min-h-0 relative"> <div className="flex-1 flex flex-col min-h-0 relative">
{actionPreview && ( {actionPreview && (
<div className="absolute inset-0 z-20 flex flex-col bg-[#F2F0E9]/95 backdrop-blur-md animate-in fade-in slide-in-from-top-4 duration-300"> <div className="absolute inset-0 z-20 flex flex-col bg-background/95 backdrop-blur-md animate-in fade-in slide-in-from-top-4 duration-300">
<div className="px-6 py-4 border-b border-border flex items-center justify-between shrink-0"> <div className="px-6 py-4 border-b border-border flex items-center justify-between shrink-0">
<p className="text-[10px] font-bold uppercase tracking-widest text-[#D4A373]">{actionPreview.label}</p> <p className="text-[10px] font-bold uppercase tracking-widest text-memento-blue">{actionPreview.label}</p>
<button onClick={handleDiscardPreview} className="text-[#1C1C1C]/40 hover:text-[#1C1C1C]"><X size={18} /></button> <button onClick={handleDiscardPreview} className="text-foreground/40 hover:text-foreground"><X size={18} /></button>
</div> </div>
<div className="flex-1 overflow-y-auto p-6 custom-scrollbar"> <div className="flex-1 overflow-y-auto p-6 custom-scrollbar">
<div className="bg-white/60 backdrop-blur-sm border border-border p-6 rounded-2xl shadow-sm leading-relaxed text-sm"> <div className="bg-card/60 backdrop-blur-sm border border-border p-6 rounded-2xl shadow-sm leading-relaxed text-sm">
<MarkdownContent content={actionPreview.text} /> <MarkdownContent content={actionPreview.text} />
</div> </div>
</div> </div>
<div className="p-6 border-t border-border flex gap-3 shrink-0"> <div className="p-6 border-t border-border flex gap-3 shrink-0">
<button onClick={handleDiscardPreview} className="flex-1 py-3.5 text-[10px] font-bold uppercase tracking-widest text-[#1C1C1C]/40 hover:text-[#1C1C1C] transition-all">ANNULER</button> <button onClick={handleDiscardPreview} className="flex-1 py-3.5 text-[10px] font-bold uppercase tracking-widest text-foreground/40 hover:text-foreground transition-all">ANNULER</button>
<button onClick={handleApplyPreview} className="flex-1 py-3.5 bg-[#1C1C1C] text-[#F2F0E9] rounded-xl text-[10px] font-bold uppercase tracking-widest shadow-lg transition-all hover:opacity-90">APPLIQUER À LA NOTE</button> <button onClick={handleApplyPreview} className="flex-1 py-3.5 bg-foreground text-background rounded-xl text-[10px] font-bold uppercase tracking-widest shadow-lg transition-all hover:opacity-90">APPLIQUER À LA NOTE</button>
</div> </div>
</div> </div>
)} )}
{resourcePreview && ( {resourcePreview && (
<div className="absolute inset-0 z-20 flex flex-col bg-[#F2F0E9]/95 backdrop-blur-md animate-in fade-in slide-in-from-top-4 duration-300"> <div className="absolute inset-0 z-20 flex flex-col bg-background/95 backdrop-blur-md animate-in fade-in slide-in-from-top-4 duration-300">
<div className="px-6 py-4 border-b border-border/40 flex items-center justify-between shrink-0"> <div className="px-6 py-4 border-b border-border/40 flex items-center justify-between shrink-0">
<p className="text-[10px] font-bold uppercase tracking-widest text-[#75B2D6]"> <p className="text-[10px] font-bold uppercase tracking-widest text-memento-blue">
{resourcePreview.source === 'chat' ? 'Injecter depuis Discussion' : 'Aperçu IA'} {resourcePreview.source === 'chat' ? 'Injecter depuis Discussion' : 'Aperçu IA'}
</p> </p>
<button onClick={() => setResourcePreview(null)} className="text-[#1C1C1C]/40 hover:text-[#1C1C1C]"> <button onClick={() => setResourcePreview(null)} className="text-foreground/40 hover:text-foreground">
<X size={18} /> <X size={18} />
</button> </button>
</div> </div>
<div className="flex-1 overflow-y-auto p-6 custom-scrollbar"> <div className="flex-1 overflow-y-auto p-6 custom-scrollbar">
<div className="bg-white/60 backdrop-blur-sm border border-border p-6 rounded-2xl shadow-sm leading-relaxed text-sm"> <div className="bg-card/60 backdrop-blur-sm border border-border p-6 rounded-2xl shadow-sm leading-relaxed text-sm">
<MarkdownContent content={resourcePreview.text} /> <MarkdownContent content={resourcePreview.text} />
</div> </div>
</div> </div>
<div className="p-6 border-t border-border flex gap-3 shrink-0"> <div className="p-6 border-t border-border flex gap-3 shrink-0">
<button onClick={() => setResourcePreview(null)} className="flex-1 py-3.5 text-[10px] font-bold uppercase tracking-widest text-[#1C1C1C]/40 hover:text-[#1C1C1C] transition-all">ANNULER</button> <button onClick={() => setResourcePreview(null)} className="flex-1 py-3.5 text-[10px] font-bold uppercase tracking-widest text-foreground/40 hover:text-foreground transition-all">ANNULER</button>
<button onClick={handleApplyResourcePreview} className="flex-1 py-3.5 bg-[#75B2D6] text-white rounded-xl text-[10px] font-bold uppercase tracking-widest shadow-lg shadow-[#75B2D6]/20 transition-all hover:opacity-90">APPLIQUER À LA NOTE</button> <button onClick={handleApplyResourcePreview} className="flex-1 py-3.5 bg-memento-blue text-white rounded-xl text-[10px] font-bold uppercase tracking-widest shadow-lg shadow-memento-blue/20 transition-all hover:opacity-90">APPLIQUER À LA NOTE</button>
</div> </div>
</div> </div>
)} )}
@@ -593,10 +601,10 @@ export function ContextualAIChat({
<div className="flex-1 overflow-y-auto p-6 custom-scrollbar space-y-8"> <div className="flex-1 overflow-y-auto p-6 custom-scrollbar space-y-8">
{messages.length === 0 && ( {messages.length === 0 && (
<div className="h-full flex flex-col items-center justify-center text-center space-y-6 py-12"> <div className="h-full flex flex-col items-center justify-center text-center space-y-6 py-12">
<div className="w-20 h-20 rounded-full bg-white/40 backdrop-blur-sm border border-dashed border-border flex items-center justify-center shadow-sm"> <div className="w-20 h-20 rounded-full bg-card/40 backdrop-blur-sm border border-dashed border-border flex items-center justify-center shadow-sm">
<MessageSquare size={32} className="text-[#D4A373]/30" /> <MessageSquare size={32} className="text-memento-blue/60" />
</div> </div>
<p className="text-xs font-serif italic text-[#1C1C1C]/40 leading-relaxed max-w-[200px]">Posez une question à l'Assistant pour commencer.</p> <p className="text-xs font-serif italic text-foreground/40 leading-relaxed max-w-[200px]">Posez une question à l'Assistant pour commencer.</p>
</div> </div>
)} )}
{messages.map((msg: UIMessage) => { {messages.map((msg: UIMessage) => {
@@ -606,14 +614,14 @@ export function ContextualAIChat({
return ( return (
<div key={msg.id} className={cn('flex flex-col gap-3', !isAssistant && 'items-end')} onMouseEnter={() => isAssistant && setHoveredMsgId(msg.id)} onMouseLeave={() => setHoveredMsgId(null)}> <div key={msg.id} className={cn('flex flex-col gap-3', !isAssistant && 'items-end')} onMouseEnter={() => isAssistant && setHoveredMsgId(msg.id)} onMouseLeave={() => setHoveredMsgId(null)}>
<div className="relative group max-w-[95%]"> <div className="relative group max-w-[95%]">
<div className={cn('p-5 rounded-2xl text-sm leading-relaxed transition-all shadow-sm', !isAssistant ? 'bg-[#1C1C1C] text-[#F2F0E9]' : 'bg-white/60 backdrop-blur-sm border border-border text-[#1C1C1C]')}> <div className={cn('p-5 rounded-2xl text-sm leading-relaxed transition-all shadow-sm', !isAssistant ? 'bg-foreground text-background' : 'bg-card/60 backdrop-blur-sm border border-border text-foreground')}>
{isAssistant ? <MarkdownContent content={content} /> : <p className="font-medium">{content}</p>} {isAssistant ? <MarkdownContent content={content} /> : <p className="font-medium">{content}</p>}
</div> </div>
{isAssistant && onApplyToNote && (hoveredMsgId === msg.id || messages.at(-1)?.id === msg.id) && ( {isAssistant && onApplyToNote && (hoveredMsgId === msg.id || messages.at(-1)?.id === msg.id) && (
<div className="flex gap-2 mt-3 opacity-0 group-hover:opacity-100 transition-all"> <div className="flex gap-2 mt-3 opacity-0 group-hover:opacity-100 transition-all">
<button onClick={() => handleInjectFromChat(content, 'replace')} className="px-3 py-1.5 rounded-lg text-[9px] font-bold uppercase tracking-widest bg-[#1C1C1C] text-[#F2F0E9] hover:opacity-90">REPLACER</button> <button onClick={() => handleInjectFromChat(content, 'replace')} className="px-3 py-1.5 rounded-lg text-[9px] font-bold uppercase tracking-widest bg-foreground text-background hover:opacity-90">REPLACER</button>
<button onClick={() => handleInjectFromChat(content, 'complete')} className="px-3 py-1.5 rounded-lg text-[9px] font-bold uppercase tracking-widest bg-white/40 backdrop-blur-sm border border-border text-[#1C1C1C] hover:bg-white/60">COMPLÉTER</button> <button onClick={() => handleInjectFromChat(content, 'complete')} className="px-3 py-1.5 rounded-lg text-[9px] font-bold uppercase tracking-widest bg-card/40 backdrop-blur-sm border border-border text-foreground hover:bg-card/60">COMPLÉTER</button>
<button onClick={() => handleInjectFromChat(content, 'merge')} className="px-3 py-1.5 rounded-lg text-[9px] font-bold uppercase tracking-widest bg-white/40 backdrop-blur-sm border border-border text-[#1C1C1C] hover:bg-white/60">FUSIONNER</button> <button onClick={() => handleInjectFromChat(content, 'merge')} className="px-3 py-1.5 rounded-lg text-[9px] font-bold uppercase tracking-widest bg-card/40 backdrop-blur-sm border border-border text-foreground hover:bg-card/60">FUSIONNER</button>
</div> </div>
)} )}
</div> </div>
@@ -622,8 +630,8 @@ export function ContextualAIChat({
})} })}
{isLoading && ( {isLoading && (
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div className="bg-white/60 backdrop-blur-sm border border-border p-5 rounded-2xl shadow-sm w-fit"> <div className="bg-card/60 backdrop-blur-sm border border-border p-5 rounded-2xl shadow-sm w-fit">
<div className="flex gap-1.5"><span className="w-1.5 h-1.5 bg-[#D4A373] rounded-full animate-pulse" /><span className="w-1.5 h-1.5 bg-[#D4A373] rounded-full animate-pulse delay-75" /><span className="w-1.5 h-1.5 bg-[#D4A373] rounded-full animate-pulse delay-150" /></div> <div className="flex gap-1.5"><span className="w-1.5 h-1.5 bg-memento-blue rounded-full animate-pulse" /><span className="w-1.5 h-1.5 bg-memento-blue rounded-full animate-pulse delay-75" /><span className="w-1.5 h-1.5 bg-memento-blue rounded-full animate-pulse delay-150" /></div>
</div> </div>
</div> </div>
)} )}
@@ -634,15 +642,15 @@ export function ContextualAIChat({
<div className="px-6 py-8 border-t border-border shrink-0 space-y-6"> <div className="px-6 py-8 border-t border-border shrink-0 space-y-6">
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-3"> <div className="space-y-3">
<label className="text-[10px] uppercase tracking-[0.25em] font-bold text-[#1C1C1C]/40 px-1">CONTEXTE</label> <label className="text-[10px] uppercase tracking-[0.25em] font-bold text-foreground/40 px-1">CONTEXTE</label>
<Select value={chatScope} onValueChange={setChatScope}> <Select value={chatScope} onValueChange={setChatScope}>
<SelectTrigger className="w-full h-10 px-3 bg-white/60 backdrop-blur-sm border border-border rounded-xl text-[11px] flex items-center justify-between cursor-pointer hover:border-[#1C1C1C]/20 transition-all outline-none ring-0 shadow-sm text-[#1C1C1C]"> <SelectTrigger className="w-full h-10 px-3 bg-card/60 backdrop-blur-sm border border-border rounded-xl text-[11px] flex items-center justify-between cursor-pointer hover:border-foreground/20 transition-all outline-none ring-0 shadow-sm text-foreground">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<BookOpen size={14} className="text-[#1C1C1C]/40" /> <BookOpen size={14} className="text-foreground/40" />
<SelectValue /> <SelectValue />
</div> </div>
</SelectTrigger> </SelectTrigger>
<SelectContent className="rounded-xl border-border shadow-xl bg-[#F2F0E9]"> <SelectContent className="rounded-xl border-border shadow-xl bg-background">
<SelectItem value="note" className="text-[11px] py-2.5 uppercase tracking-wider font-bold">Cette note</SelectItem> <SelectItem value="note" className="text-[11px] py-2.5 uppercase tracking-wider font-bold">Cette note</SelectItem>
<SelectItem value="all" className="text-[11px] py-2.5 uppercase tracking-wider font-bold">Tout Momento</SelectItem> <SelectItem value="all" className="text-[11px] py-2.5 uppercase tracking-wider font-bold">Tout Momento</SelectItem>
{notebooks.map(nb => ( {notebooks.map(nb => (
@@ -652,7 +660,7 @@ export function ContextualAIChat({
</Select> </Select>
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
<label className="text-[10px] uppercase tracking-[0.25em] font-bold text-[#1C1C1C]/40 px-1">TON D'ÉCRITURE</label> <label className="text-[10px] uppercase tracking-[0.25em] font-bold text-foreground/40 px-1">TON D'ÉCRITURE</label>
<div className="grid grid-cols-4 gap-1.5"> <div className="grid grid-cols-4 gap-1.5">
{TONES.map((tone) => { {TONES.map((tone) => {
const Icon = tone.icon const Icon = tone.icon
@@ -664,11 +672,11 @@ export function ContextualAIChat({
className={cn( className={cn(
'h-[52px] rounded-xl border transition-all flex flex-col items-center justify-center gap-1.5 shadow-sm', 'h-[52px] rounded-xl border transition-all flex flex-col items-center justify-center gap-1.5 shadow-sm',
isActive isActive
? 'bg-[#ACB995]/10 border-[#ACB995] text-[#ACB995]' ? 'bg-memento-blue/10 border-memento-blue text-memento-blue'
: 'bg-white/60 border-border text-[#1C1C1C]/40 hover:border-[#1C1C1C]/20' : 'bg-card/60 border-border text-foreground/40 hover:border-foreground/20'
)} )}
> >
<Icon size={14} className={isActive ? 'text-[#ACB995]' : 'text-[#1C1C1C]/40'} /> <Icon size={14} className={isActive ? 'text-memento-blue' : 'text-foreground/40'} />
<span className="text-[9px] font-bold uppercase tracking-tight">{tone.label}</span> <span className="text-[9px] font-bold uppercase tracking-tight">{tone.label}</span>
</button> </button>
) )
@@ -680,7 +688,7 @@ export function ContextualAIChat({
<div className="relative"> <div className="relative">
<textarea <textarea
rows={4} rows={4}
className="w-full bg-white/60 border border-border rounded-2xl p-5 pr-14 text-sm outline-none focus:border-[#ACB995] transition-all resize-none leading-relaxed font-light custom-scrollbar shadow-sm text-[#1C1C1C]" className="w-full bg-card/60 border border-border rounded-2xl p-5 pr-14 text-sm outline-none focus:border-memento-blue transition-all resize-none leading-relaxed font-light custom-scrollbar shadow-sm text-foreground"
placeholder="Posez votre question sur cette note..." placeholder="Posez votre question sur cette note..."
value={input} value={input}
onChange={e => setInput(e.target.value)} onChange={e => setInput(e.target.value)}
@@ -690,17 +698,17 @@ export function ContextualAIChat({
<div className="absolute right-4 bottom-4 flex gap-2"> <div className="absolute right-4 bottom-4 flex gap-2">
<button <button
onClick={() => setWebSearch(!webSearch)} onClick={() => setWebSearch(!webSearch)}
className={cn("p-2.5 rounded-xl transition-colors", webSearch ? "text-[#ACB995] bg-[#ACB995]/10" : "text-[#1C1C1C]/20 hover:text-[#1C1C1C]")} className={cn("p-2.5 rounded-xl transition-colors", webSearch ? "text-memento-blue bg-memento-blue/10" : "text-foreground/20 hover:text-foreground")}
title="Web Search" title="Web Search"
> >
<Globe size={18} /> <Globe size={18} />
</button> </button>
<button onClick={handleSend} disabled={!input.trim() || isLoading} className="p-2.5 bg-[#ACB995] text-white rounded-xl transition-all hover:scale-105 active:scale-95 shadow-lg shadow-[#ACB995]/20 disabled:opacity-30"> <button onClick={handleSend} disabled={!input.trim() || isLoading} className="p-2.5 bg-memento-blue text-white rounded-xl transition-all hover:scale-105 active:scale-95 shadow-lg shadow-memento-blue/20 disabled:opacity-30">
<Send size={18} /> <Send size={18} />
</button> </button>
</div> </div>
</div> </div>
<p className="text-[9px] text-[#1C1C1C]/30 text-center mt-2 uppercase tracking-[0.2em] font-bold italic">Maj+Entrée = nouvelle ligne</p> <p className="text-[9px] text-foreground/30 text-center mt-2 uppercase tracking-[0.2em] font-bold italic">Maj+Entrée = nouvelle ligne</p>
</div> </div>
</motion.div> </motion.div>
)} )}
@@ -716,7 +724,7 @@ export function ContextualAIChat({
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="h-px flex-1 bg-border/40" /> <div className="h-px flex-1 bg-border/40" />
<h4 className="text-[9px] uppercase tracking-[0.3em] font-bold text-[#1C1C1C]/40 whitespace-nowrap">{t('ai.transformations')}</h4> <h4 className="text-[9px] uppercase tracking-[0.3em] font-bold text-foreground/40 whitespace-nowrap">{t('ai.transformations')}</h4>
<div className="h-px flex-1 bg-border/40" /> <div className="h-px flex-1 bg-border/40" />
</div> </div>
@@ -725,7 +733,7 @@ export function ContextualAIChat({
initial={{ opacity: 0, y: -10 }} initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
onClick={onUndoLastAction} onClick={onUndoLastAction}
className="w-full py-3.5 bg-[#D4A373]/20 border border-[#D4A373]/50 rounded-xl flex items-center justify-center gap-2 text-[11px] font-bold text-[#D4A373] uppercase tracking-[0.2em] hover:bg-[#D4A373]/30 transition-all shadow-md" className="w-full py-3.5 bg-memento-blue/20 border border-memento-blue/50 rounded-xl flex items-center justify-center gap-2 text-[11px] font-bold text-memento-blue uppercase tracking-[0.2em] hover:bg-memento-blue/30 transition-all shadow-md"
> >
<RotateCcw size={12} /> {t('ai.undoLastAction')} <RotateCcw size={12} /> {t('ai.undoLastAction')}
</motion.button> </motion.button>
@@ -736,11 +744,11 @@ export function ContextualAIChat({
const isActive = action.id === 'translate' && showLangPicker const isActive = action.id === 'translate' && showLangPicker
const Icon = action.icon const Icon = action.icon
return ( return (
<button key={i} onClick={() => action.id === 'translate' ? setShowLangPicker(v => !v) : handleAction(action)} disabled={!!actionLoading} className={cn("flex flex-col items-center gap-3 p-4 bg-white/40 backdrop-blur-sm border rounded-xl transition-all group shadow-sm", isActive ? "border-[#ACB995] bg-[#ACB995]/5" : "border-border hover:border-[#1C1C1C]/20")}> <button key={i} onClick={() => action.id === 'translate' ? setShowLangPicker(v => !v) : handleAction(action)} disabled={!!actionLoading} className={cn("flex flex-col items-center gap-3 p-4 bg-card/40 backdrop-blur-sm border rounded-xl transition-all group shadow-sm", isActive ? "border-memento-blue bg-memento-blue/5" : "border-border hover:border-foreground/20")}>
<div className={cn("p-2 rounded-lg bg-white/60 transition-colors group-hover:bg-[#1C1C1C] group-hover:text-[#F2F0E9] shadow-sm", loading && "animate-pulse", isActive && "bg-[#ACB995] text-white")}> <div className={cn("p-2 rounded-lg bg-card/60 transition-colors group-hover:bg-foreground group-hover:text-background shadow-sm", loading && "animate-pulse", isActive && "bg-memento-blue text-white")}>
{loading ? <Loader2 size={14} className="animate-spin" /> : <Icon size={14} />} {loading ? <Loader2 size={14} className="animate-spin" /> : <Icon size={14} />}
</div> </div>
<span className={cn("text-[10px] font-bold uppercase tracking-widest", isActive ? "text-[#ACB995]" : "text-[#1C1C1C]/80")}>{t(action.i18nKey)}</span> <span className={cn("text-[10px] font-bold uppercase tracking-widest", isActive ? "text-memento-blue" : "text-foreground/80")}>{t(action.i18nKey)}</span>
</button> </button>
) )
})} })}
@@ -754,7 +762,7 @@ export function ContextualAIChat({
exit={{ height: 0, opacity: 0 }} exit={{ height: 0, opacity: 0 }}
className="col-span-2 overflow-hidden" className="col-span-2 overflow-hidden"
> >
<div className="mt-2 p-5 bg-white/40 backdrop-blur-sm border border-[#75B2D6]/30 rounded-2xl space-y-5 shadow-sm"> <div className="mt-2 p-5 bg-card/40 backdrop-blur-sm border border-memento-blue/30 rounded-2xl space-y-5 shadow-sm">
<div className="grid grid-cols-3 gap-2"> <div className="grid grid-cols-3 gap-2">
{['Français', 'English', 'Español', 'Deutsch', 'Persan', 'Portugais', 'Italiano', 'Chinois', 'Japonais'].map((lang) => ( {['Français', 'English', 'Español', 'Deutsch', 'Persan', 'Portugais', 'Italiano', 'Chinois', 'Japonais'].map((lang) => (
<button <button
@@ -763,8 +771,8 @@ export function ContextualAIChat({
className={cn( className={cn(
"py-2 px-1 rounded-lg border text-[10px] font-bold uppercase tracking-tighter transition-all", "py-2 px-1 rounded-lg border text-[10px] font-bold uppercase tracking-tighter transition-all",
translateTarget === lang translateTarget === lang
? "bg-[#ACB995] border-[#ACB995] text-white shadow-md shadow-[#ACB995]/20" ? "bg-memento-blue border-memento-blue text-white shadow-md shadow-memento-blue/20"
: "bg-white/60 border-border text-[#1C1C1C]/60 hover:border-[#1C1C1C]/20" : "bg-card/60 border-border text-foreground/60 hover:border-foreground/20"
)} )}
> >
{lang} {lang}
@@ -773,7 +781,7 @@ export function ContextualAIChat({
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<span className="text-[8px] uppercase tracking-[0.2em] font-bold text-[#1C1C1C]/40 px-1">{t('ai.otherLanguage')}</span> <span className="text-[8px] uppercase tracking-[0.2em] font-bold text-foreground/40 px-1">{t('ai.otherLanguage')}</span>
<input <input
type="text" type="text"
value={customLangInput} value={customLangInput}
@@ -782,14 +790,14 @@ export function ContextualAIChat({
setTranslateTarget(e.target.value) setTranslateTarget(e.target.value)
}} }}
placeholder="ex: Arabe, Russe..." placeholder="ex: Arabe, Russe..."
className="w-full bg-white/60 border border-border rounded-xl px-4 py-2.5 text-[11px] outline-none focus:border-[#75B2D6] transition-all text-[#1C1C1C]" className="w-full bg-card/60 border border-border rounded-xl px-4 py-2.5 text-[11px] outline-none focus:border-memento-blue transition-all text-foreground"
/> />
</div> </div>
<button <button
onClick={() => handleAction(ACTION_IDS.find(a => a.id === 'translate')!, translateTarget)} onClick={() => handleAction(ACTION_IDS.find(a => a.id === 'translate')!, translateTarget)}
disabled={!translateTarget || !!actionLoading} disabled={!translateTarget || !!actionLoading}
className="w-full py-3 bg-[#75B2D6] text-white rounded-xl text-[10px] font-bold uppercase tracking-[0.2em] flex items-center justify-center gap-2 hover:opacity-90 disabled:opacity-50 shadow-lg shadow-[#75B2D6]/20" className="w-full py-3 bg-memento-blue text-white rounded-xl text-[10px] font-bold uppercase tracking-[0.2em] flex items-center justify-center gap-2 hover:opacity-90 disabled:opacity-50 shadow-lg shadow-memento-blue/20"
> >
<Languages size={14} /> {t('ai.translateNow')} <Languages size={14} /> {t('ai.translateNow')}
</button> </button>
@@ -802,18 +810,18 @@ export function ContextualAIChat({
<button <button
onClick={() => handleAction(ACTION_IDS.find(a => a.id === 'markdown')!)} onClick={() => handleAction(ACTION_IDS.find(a => a.id === 'markdown')!)}
disabled={!!actionLoading} disabled={!!actionLoading}
className="col-span-2 flex items-center justify-center gap-3 py-3.5 bg-white/40 backdrop-blur-sm border border-border rounded-xl text-[10px] font-bold text-[#1C1C1C]/80 hover:bg-white/60 transition-all uppercase tracking-[0.2em] shadow-sm disabled:opacity-50" className="col-span-2 flex items-center justify-center gap-3 py-3.5 bg-card/40 backdrop-blur-sm border border-border rounded-xl text-[10px] font-bold text-foreground/80 hover:bg-card/60 transition-all uppercase tracking-[0.2em] shadow-sm disabled:opacity-50"
> >
<Code size={14} className="text-[#1C1C1C]/40" /> <Code size={14} className="text-foreground/40" />
{actionLoading === 'markdown' ? <Loader2 size={14} className="animate-spin" /> : t('ai.action.toMarkdown')} {actionLoading === 'markdown' ? <Loader2 size={14} className="animate-spin" /> : t('ai.action.toMarkdown')}
</button> </button>
) : ( ) : (
<button <button
onClick={() => handleAction(ACTION_IDS.find(a => a.id === 'toRichText')!)} onClick={() => handleAction(ACTION_IDS.find(a => a.id === 'toRichText')!)}
disabled={!!actionLoading} disabled={!!actionLoading}
className="col-span-2 flex items-center justify-center gap-3 py-3.5 bg-white/40 backdrop-blur-sm border border-border rounded-xl text-[10px] font-bold text-[#1C1C1C]/80 hover:bg-white/60 transition-all uppercase tracking-[0.2em] shadow-sm disabled:opacity-50" className="col-span-2 flex items-center justify-center gap-3 py-3.5 bg-card/40 backdrop-blur-sm border border-border rounded-xl text-[10px] font-bold text-foreground/80 hover:bg-card/60 transition-all uppercase tracking-[0.2em] shadow-sm disabled:opacity-50"
> >
<Wand2 size={14} className="text-[#1C1C1C]/40" /> <Wand2 size={14} className="text-foreground/40" />
{actionLoading === 'toRichText' ? <Loader2 size={14} className="animate-spin" /> : t('ai.action.toRichText')} {actionLoading === 'toRichText' ? <Loader2 size={14} className="animate-spin" /> : t('ai.action.toRichText')}
</button> </button>
)} )}
@@ -823,41 +831,41 @@ export function ContextualAIChat({
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="h-px flex-1 bg-border/40" /> <div className="h-px flex-1 bg-border/40" />
<h4 className="text-[9px] uppercase tracking-[0.3em] font-bold text-[#1C1C1C]/40 whitespace-nowrap">{t('ai.generationTools')}</h4> <h4 className="text-[9px] uppercase tracking-[0.3em] font-bold text-foreground/40 whitespace-nowrap">{t('ai.generationTools')}</h4>
<div className="h-px flex-1 bg-border/40" /> <div className="h-px flex-1 bg-border/40" />
</div> </div>
<div className="group relative p-6 rounded-2xl bg-white/40 backdrop-blur-sm border border-border hover:border-[#75B2D6]/30 transition-all duration-500 overflow-hidden shadow-sm"> <div className="group relative p-6 rounded-2xl bg-card/40 backdrop-blur-sm border border-border hover:border-memento-blue/30 transition-all duration-500 overflow-hidden shadow-sm">
<div className="absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity"> <div className="absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
<Layout size={80} className="text-[#75B2D6]" /> <Layout size={80} className="text-memento-blue" />
</div> </div>
<div className="relative space-y-5"> <div className="relative space-y-5">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 bg-white/60 rounded-lg text-[#75B2D6]"><Layout size={18} /></div> <div className="p-2 bg-card/60 rounded-lg text-memento-blue"><Layout size={18} /></div>
<div className="space-y-0.5"> <div className="space-y-0.5">
<h5 className="text-sm font-bold text-[#1C1C1C] leading-none">{t('ai.generate.slides')}</h5> <h5 className="text-sm font-bold text-foreground leading-none">{t('ai.generate.slides')}</h5>
<p className="text-[9px] text-[#1C1C1C]/40 uppercase tracking-tight">{t('ai.generate.sectionLabel')}</p> <p className="text-[9px] text-foreground/40 uppercase tracking-tight">{t('ai.generate.sectionLabel')}</p>
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5"> <div className="space-y-1.5">
<span className="text-[8px] uppercase tracking-[0.2em] font-bold text-[#1C1C1C]/40 px-1">{t('ai.generate.theme')}</span> <span className="text-[8px] uppercase tracking-[0.2em] font-bold text-foreground/40 px-1">{t('ai.generate.theme')}</span>
<select value={slideTheme} onChange={e => setSlideTheme(e.target.value)} className="w-full bg-white/60 border border-border rounded-lg px-2 py-2 text-[10px] outline-none focus:ring-1 ring-[#75B2D6]/10 transition-all cursor-pointer text-[#1C1C1C]"> <select value={slideTheme} onChange={e => setSlideTheme(e.target.value)} className="w-full bg-card/60 border border-border rounded-lg px-2 py-2 text-[10px] outline-none focus:ring-1 ring-memento-blue/10 transition-all cursor-pointer text-foreground">
<option value="architectural_mono">{t('ai.generate.themeArchitecturalMono')}</option> <option value="architectural_mono">{t('ai.generate.themeArchitecturalMono')}</option>
<option value="vibrant_tech">{t('ai.generate.themeVibrantTech')}</option> <option value="vibrant_tech">{t('ai.generate.themeVibrantTech')}</option>
<option value="minimal_silk">{t('ai.generate.themeMinimalSilk')}</option> <option value="minimal_silk">{t('ai.generate.themeMinimalSilk')}</option>
</select> </select>
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<span className="text-[8px] uppercase tracking-[0.2em] font-bold text-[#1C1C1C]/40 px-1">{t('ai.generate.style')}</span> <span className="text-[8px] uppercase tracking-[0.2em] font-bold text-foreground/40 px-1">{t('ai.generate.style')}</span>
<select value={slideStyle} onChange={e => setSlideStyle(e.target.value)} className="w-full bg-white/60 border border-border rounded-lg px-2 py-2 text-[10px] outline-none focus:ring-1 ring-[#75B2D6]/10 transition-all cursor-pointer text-[#1C1C1C]"> <select value={slideStyle} onChange={e => setSlideStyle(e.target.value)} className="w-full bg-card/60 border border-border rounded-lg px-2 py-2 text-[10px] outline-none focus:ring-1 ring-memento-blue/10 transition-all cursor-pointer text-foreground">
<option value="professional">{t('ai.generate.styleProfessional')}</option> <option value="professional">{t('ai.generate.styleProfessional')}</option>
<option value="creative">Creative</option> <option value="creative">Creative</option>
<option value="brutalist">Brutalist</option> <option value="brutalist">Brutalist</option>
</select> </select>
</div> </div>
</div> </div>
<button onClick={() => handleGenerate('slides')} disabled={!!generateLoading} className="w-full py-3.5 bg-[#75B2D6] text-white rounded-xl text-[10px] font-bold flex items-center justify-center gap-2 hover:opacity-90 transition-all shadow-lg shadow-[#75B2D6]/20 uppercase tracking-[0.2em] disabled:opacity-50"> <button onClick={() => handleGenerate('slides')} disabled={!!generateLoading} className="w-full py-3.5 bg-memento-blue text-white rounded-xl text-[10px] font-bold flex items-center justify-center gap-2 hover:opacity-90 transition-all shadow-lg shadow-memento-blue/20 uppercase tracking-[0.2em] disabled:opacity-50">
{generateLoading === 'slides' ? <Loader2 size={14} className="animate-spin" /> : <><Presentation size={14} className="opacity-80" /> {t('ai.generating')}</>} {generateLoading === 'slides' ? <Loader2 size={14} className="animate-spin" /> : <><Presentation size={14} className="opacity-80" /> {t('ai.generating')}</>}
</button> </button>
@@ -865,17 +873,17 @@ export function ContextualAIChat({
<motion.div <motion.div
initial={{ opacity: 0, y: 10 }} initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
className="mt-4 p-4 bg-[#75B2D6]/10 border border-[#75B2D6]/20 rounded-xl space-y-3" className="mt-4 p-4 bg-memento-blue/10 border border-memento-blue/20 rounded-xl space-y-3"
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-[9px] font-bold text-[#75B2D6] uppercase tracking-widest flex items-center gap-1.5"> <span className="text-[9px] font-bold text-memento-blue uppercase tracking-widest flex items-center gap-1.5">
<Check size={12} /> Présentation prête <Check size={12} /> Présentation prête
</span> </span>
<a <a
href={`/lab?id=${generateResult.canvasId}`} href={`/lab?id=${generateResult.canvasId}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="p-1.5 bg-white/60 rounded-lg text-[#1C1C1C]/50 hover:text-[#1C1C1C] hover:bg-white transition-colors" className="p-1.5 bg-card/60 rounded-lg text-foreground/50 hover:text-foreground hover:bg-card transition-colors"
title="Voir dans L'Atelier" title="Voir dans L'Atelier"
> >
<ExternalLink size={12} /> <ExternalLink size={12} />
@@ -905,7 +913,7 @@ export function ContextualAIChat({
mToast.error('Échec du téléchargement') mToast.error('Échec du téléchargement')
} }
}} }}
className="flex items-center justify-center gap-2 w-full py-2.5 bg-[#ACB995] text-white rounded-lg text-[10px] font-bold uppercase tracking-[0.15em] hover:opacity-90 transition-opacity shadow-sm" className="flex items-center justify-center gap-2 w-full py-2.5 bg-memento-blue text-white rounded-lg text-[10px] font-bold uppercase tracking-[0.15em] hover:opacity-90 transition-opacity shadow-sm"
> >
<Download size={13} /> <Download size={13} />
Télécharger .pptx Télécharger .pptx
@@ -916,22 +924,22 @@ export function ContextualAIChat({
</div> </div>
</div> </div>
<div className="group relative p-6 rounded-2xl bg-white/40 backdrop-blur-sm border border-border hover:border-[#A3B18A]/30 transition-all duration-500 overflow-hidden shadow-sm"> <div className="group relative p-6 rounded-2xl bg-card/40 backdrop-blur-sm border border-border hover:border-primary/30 transition-all duration-500 overflow-hidden shadow-sm">
<div className="absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity"> <div className="absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
<BookOpen size={80} className="text-[#A3B18A]" /> <BookOpen size={80} className="text-primary" />
</div> </div>
<div className="relative space-y-5"> <div className="relative space-y-5">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 bg-white/60 rounded-lg text-[#A3B18A]"><BookOpen size={18} /></div> <div className="p-2 bg-card/60 rounded-lg text-primary"><BookOpen size={18} /></div>
<div className="space-y-0.5"> <div className="space-y-0.5">
<h5 className="text-sm font-bold text-[#1C1C1C] leading-none">{t('ai.generate.diagram')}</h5> <h5 className="text-sm font-bold text-foreground leading-none">{t('ai.generate.diagram')}</h5>
<p className="text-[9px] text-[#1C1C1C]/40 uppercase tracking-tight">{t('ai.generate.diagramReadyHint')}</p> <p className="text-[9px] text-foreground/40 uppercase tracking-tight">{t('ai.generate.diagramReadyHint')}</p>
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5"> <div className="space-y-1.5">
<span className="text-[8px] uppercase tracking-[0.2em] font-bold text-[#1C1C1C]/40 px-1">{t('ai.generate.diagramType')}</span> <span className="text-[8px] uppercase tracking-[0.2em] font-bold text-foreground/40 px-1">{t('ai.generate.diagramType')}</span>
<select value={diagramType} onChange={e => setDiagramType(e.target.value)} className="w-full bg-white/60 border border-border rounded-lg px-2 py-2 text-[10px] outline-none focus:ring-1 ring-[#A3B18A]/10 transition-all cursor-pointer text-[#1C1C1C]"> <select value={diagramType} onChange={e => setDiagramType(e.target.value)} className="w-full bg-card/60 border border-border rounded-lg px-2 py-2 text-[10px] outline-none focus:ring-1 ring-primary/10 transition-all cursor-pointer text-foreground">
<option value="auto">{t('ai.generate.typeAuto')}</option> <option value="auto">{t('ai.generate.typeAuto')}</option>
<option value="flowchart">{t('ai.generate.typeFlowchart')}</option> <option value="flowchart">{t('ai.generate.typeFlowchart')}</option>
<option value="mind_map">{t('ai.generate.typeMindMap')}</option> <option value="mind_map">{t('ai.generate.typeMindMap')}</option>
@@ -942,8 +950,8 @@ export function ContextualAIChat({
</select> </select>
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<span className="text-[8px] uppercase tracking-[0.2em] font-bold text-[#1C1C1C]/40 px-1">{t('ai.generate.style')}</span> <span className="text-[8px] uppercase tracking-[0.2em] font-bold text-foreground/40 px-1">{t('ai.generate.style')}</span>
<select value={diagramStyle} onChange={e => setDiagramStyle(e.target.value)} className="w-full bg-white/60 border border-border rounded-lg px-2 py-2 text-[10px] outline-none focus:ring-1 ring-[#A3B18A]/10 transition-all cursor-pointer text-[#1C1C1C]"> <select value={diagramStyle} onChange={e => setDiagramStyle(e.target.value)} className="w-full bg-card/60 border border-border rounded-lg px-2 py-2 text-[10px] outline-none focus:ring-1 ring-primary/10 transition-all cursor-pointer text-foreground">
<option value="sketchy">{t('ai.generate.styleSketchy')}</option> <option value="sketchy">{t('ai.generate.styleSketchy')}</option>
<option value="soft">{t('ai.generate.styleSoft')}</option> <option value="soft">{t('ai.generate.styleSoft')}</option>
<option value="minimal">{t('ai.generate.styleMinimal')}</option> <option value="minimal">{t('ai.generate.styleMinimal')}</option>
@@ -954,7 +962,7 @@ export function ContextualAIChat({
</select> </select>
</div> </div>
</div> </div>
<button onClick={() => handleGenerate('diagram')} disabled={!!generateLoading} className="w-full py-3.5 bg-[#A3B18A] text-white rounded-xl text-[10px] font-bold flex items-center justify-center gap-2 hover:opacity-90 transition-all shadow-lg shadow-[#A3B18A]/20 uppercase tracking-[0.2em] disabled:opacity-50"> <button onClick={() => handleGenerate('diagram')} disabled={!!generateLoading} className="w-full py-3.5 bg-primary text-primary-foreground rounded-xl text-[10px] font-bold flex items-center justify-center gap-2 hover:opacity-90 transition-all shadow-lg shadow-primary/20 uppercase tracking-[0.2em] disabled:opacity-50">
{generateLoading === 'diagram' ? <Loader2 size={14} className="animate-spin" /> : <>{t('ai.generating')} <ArrowRightLeft size={14} className="opacity-60" /></>} {generateLoading === 'diagram' ? <Loader2 size={14} className="animate-spin" /> : <>{t('ai.generating')} <ArrowRightLeft size={14} className="opacity-60" /></>}
</button> </button>
@@ -962,16 +970,16 @@ export function ContextualAIChat({
<motion.div <motion.div
initial={{ opacity: 0, y: 10 }} initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
className="mt-4 p-4 bg-[#A3B18A]/10 border border-[#A3B18A]/20 rounded-xl space-y-3" className="mt-4 p-4 bg-primary/10 border border-primary/20 rounded-xl space-y-3"
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-[9px] font-bold text-[#A3B18A] uppercase tracking-widest">{t('ai.generate.diagramReady')}</span> <span className="text-[9px] font-bold text-primary uppercase tracking-widest">{t('ai.generate.diagramReady')}</span>
<div className="flex gap-2"> <div className="flex gap-2">
<a <a
href={`/lab?id=${generateResult.canvasId}`} href={`/lab?id=${generateResult.canvasId}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="p-2 bg-white/60 rounded-lg text-[#1C1C1C] hover:bg-white transition-colors" className="p-2 bg-card/60 rounded-lg text-foreground hover:bg-card transition-colors"
title={t('ai.generate.openInExcalidraw')} title={t('ai.generate.openInExcalidraw')}
> >
<ExternalLink size={14} /> <ExternalLink size={14} />
@@ -979,7 +987,7 @@ export function ContextualAIChat({
<button <button
onClick={() => handleEmbedDiagramInNote(generateResult.canvasId!)} onClick={() => handleEmbedDiagramInNote(generateResult.canvasId!)}
disabled={diagramEmbedLoading} disabled={diagramEmbedLoading}
className="p-2 bg-[#A3B18A] text-white rounded-lg hover:opacity-90 transition-opacity disabled:opacity-50" className="p-2 bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity disabled:opacity-50"
title={t('ai.generate.insertDiagramInNote')} title={t('ai.generate.insertDiagramInNote')}
> >
{diagramEmbedLoading ? <Loader2 size={14} className="animate-spin" /> : <ImagePlus size={14} />} {diagramEmbedLoading ? <Loader2 size={14} className="animate-spin" /> : <ImagePlus size={14} />}
@@ -1009,32 +1017,32 @@ export function ContextualAIChat({
> >
<div className="space-y-6"> <div className="space-y-6">
<div className="space-y-3"> <div className="space-y-3">
<label className="text-[9px] uppercase tracking-[0.3em] font-bold text-[#1C1C1C]/40">{t('ai.resource.urlLabel')}</label> <label className="text-[9px] uppercase tracking-[0.3em] font-bold text-foreground/40">{t('ai.resource.urlLabel')}</label>
<div className="relative"> <div className="relative">
<input type="text" placeholder="https://..." className="w-full bg-white/40 backdrop-blur-sm border border-border rounded-xl pl-4 pr-12 py-3.5 text-xs outline-none focus:border-[#75B2D6] transition-colors shadow-sm text-[#1C1C1C]" value={resourceUrl} onChange={e => setResourceUrl(e.target.value)} /> <input type="text" placeholder="https://..." className="w-full bg-card/40 backdrop-blur-sm border border-border rounded-xl pl-4 pr-12 py-3.5 text-xs outline-none focus:border-memento-blue transition-colors shadow-sm text-foreground" value={resourceUrl} onChange={e => setResourceUrl(e.target.value)} />
<Globe size={16} className="absolute right-4 top-1/2 -translate-y-1/2 text-[#1C1C1C]/20" /> <Globe size={16} className="absolute right-4 top-1/2 -translate-y-1/2 text-foreground/20" />
</div> </div>
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
<label className="text-[9px] uppercase tracking-[0.3em] font-bold text-[#1C1C1C]/40">{t('ai.resource.resourceText')}</label> <label className="text-[9px] uppercase tracking-[0.3em] font-bold text-foreground/40">{t('ai.resource.resourceText')}</label>
<textarea rows={10} placeholder={t('ai.resource.resourcePlaceholder')} className="w-full bg-white/40 backdrop-blur-sm border border-border rounded-xl p-5 text-sm outline-none focus:border-[#75B2D6] transition-colors resize-none leading-relaxed font-light shadow-sm text-[#1C1C1C]" value={resourceText} onChange={e => setResourceText(e.target.value)} /> <textarea rows={10} placeholder={t('ai.resource.resourcePlaceholder')} className="w-full bg-card/40 backdrop-blur-sm border border-border rounded-xl p-5 text-sm outline-none focus:border-memento-blue transition-colors resize-none leading-relaxed font-light shadow-sm text-foreground" value={resourceText} onChange={e => setResourceText(e.target.value)} />
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<label className="text-[9px] uppercase tracking-[0.3em] font-bold text-[#1C1C1C]/40">{t('ai.resource.integrationMode')}</label> <label className="text-[9px] uppercase tracking-[0.3em] font-bold text-foreground/40">{t('ai.resource.integrationMode')}</label>
<div className="grid grid-cols-3 gap-2"> <div className="grid grid-cols-3 gap-2">
{[ {[
{ id: 'replace', label: t('ai.resource.modeReplace'), sub: t('ai.resource.modeReplaceDesc') }, { id: 'replace', label: t('ai.resource.modeReplace'), sub: t('ai.resource.modeReplaceDesc') },
{ id: 'complete', label: t('ai.resource.modeComplete'), sub: t('ai.resource.modeCompleteDesc') }, { id: 'complete', label: t('ai.resource.modeComplete'), sub: t('ai.resource.modeCompleteDesc') },
{ id: 'merge', label: t('ai.resource.modeMerge'), sub: t('ai.resource.modeMergeDesc') }, { id: 'merge', label: t('ai.resource.modeMerge'), sub: t('ai.resource.modeMergeDesc') },
].map((mode) => ( ].map((mode) => (
<button key={mode.id} onClick={() => setResourceMode(mode.id as any)} className={cn("flex flex-col items-center justify-center p-3 rounded-xl border transition-all text-center", resourceMode === mode.id ? 'bg-[#75B2D6]/10 border-[#75B2D6] ring-1 ring-[#75B2D6]/20' : 'bg-white/40 backdrop-blur-sm border-border hover:bg-white/60 shadow-sm')}> <button key={mode.id} onClick={() => setResourceMode(mode.id as any)} className={cn("flex flex-col items-center justify-center p-3 rounded-xl border transition-all text-center", resourceMode === mode.id ? 'bg-memento-blue/10 border-memento-blue ring-1 ring-memento-blue/20' : 'bg-card/40 backdrop-blur-sm border-border hover:bg-card/60 shadow-sm')}>
<span className={cn("text-[10px] font-bold uppercase tracking-wider", resourceMode === mode.id ? 'text-[#75B2D6]' : 'text-[#1C1C1C]')}>{mode.label}</span> <span className={cn("text-[10px] font-bold uppercase tracking-wider", resourceMode === mode.id ? 'text-memento-blue' : 'text-foreground')}>{mode.label}</span>
<span className="text-[8px] text-[#1C1C1C]/40 leading-tight mt-1 font-medium">{mode.sub}</span> <span className="text-[8px] text-foreground/40 leading-tight mt-1 font-medium">{mode.sub}</span>
</button> </button>
))} ))}
</div> </div>
</div> </div>
<button onClick={handleResourcePreview} disabled={resourceEnriching || resourceScraping || !resourceText.trim()} className="w-full py-4 bg-[#75B2D6] text-white rounded-xl text-[11px] font-bold uppercase tracking-[0.2em] flex items-center justify-center gap-3 hover:opacity-90 transition-opacity shadow-lg shadow-[#75B2D6]/20 disabled:opacity-50"> <button onClick={handleResourcePreview} disabled={resourceEnriching || resourceScraping || !resourceText.trim()} className="w-full py-4 bg-memento-blue text-white rounded-xl text-[11px] font-bold uppercase tracking-[0.2em] flex items-center justify-center gap-3 hover:opacity-90 transition-opacity shadow-lg shadow-memento-blue/20 disabled:opacity-50">
{resourceEnriching || resourceScraping ? <Loader2 size={18} className="animate-spin" /> : <Sparkles size={18} />} {resourceEnriching || resourceScraping ? <Loader2 size={18} className="animate-spin" /> : <Sparkles size={18} />}
{t('ai.resource.generatePreview')} {t('ai.resource.generatePreview')}
</button> </button>

View File

@@ -9,7 +9,11 @@ import { useEffect } from 'react'
export function DirectionInitializer() { export function DirectionInitializer() {
useEffect(() => { useEffect(() => {
try { try {
const lang = localStorage.getItem('user-language') let lang = localStorage.getItem('user-language')
if (!lang) {
const c = document.cookie.split(';').map(s => s.trim()).find(s => s.startsWith('user-language='))
if (c) lang = c.split('=')[1]
}
if (lang === 'fa' || lang === 'ar') { if (lang === 'fa' || lang === 'ar') {
document.documentElement.dir = 'rtl' document.documentElement.dir = 'rtl'
document.documentElement.lang = lang document.documentElement.lang = lang

View File

@@ -11,7 +11,7 @@ import { NotesEditorialView } from '@/components/notes-editorial-view'
import { MemoryEchoNotification } from '@/components/memory-echo-notification' import { MemoryEchoNotification } from '@/components/memory-echo-notification'
import { NotebookSuggestionToast } from '@/components/notebook-suggestion-toast' import { NotebookSuggestionToast } from '@/components/notebook-suggestion-toast'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Plus, ArrowUpDown, Search, Sparkles } from 'lucide-react' import { Plus, ArrowUpDown, Search, Sparkles, FileText } from 'lucide-react'
import { useNoteRefresh } from '@/context/NoteRefreshContext' import { useNoteRefresh } from '@/context/NoteRefreshContext'
import { useRefresh } from '@/lib/use-refresh' import { useRefresh } from '@/lib/use-refresh'
import { useReminderCheck } from '@/hooks/use-reminder-check' import { useReminderCheck } from '@/hooks/use-reminder-check'
@@ -38,12 +38,17 @@ const AutoLabelSuggestionDialog = dynamic(
() => import('@/components/auto-label-suggestion-dialog').then(m => ({ default: m.AutoLabelSuggestionDialog })), () => import('@/components/auto-label-suggestion-dialog').then(m => ({ default: m.AutoLabelSuggestionDialog })),
{ ssr: false } { ssr: false }
) )
const NotebookSummaryDialog = dynamic(
() => import('@/components/notebook-summary-dialog').then(m => ({ default: m.NotebookSummaryDialog })),
{ ssr: false }
)
type InitialSettings = { type InitialSettings = {
showRecentNotes: boolean showRecentNotes: boolean
notesViewMode: 'masonry' | 'tabs' | 'list' notesViewMode: 'masonry' | 'tabs' | 'list'
noteHistory: boolean noteHistory: boolean
noteHistoryMode: 'manual' | 'auto' noteHistoryMode: 'manual' | 'auto'
aiAssistantEnabled: boolean
} }
interface HomeClientProps { interface HomeClientProps {
@@ -83,6 +88,7 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
const { shouldSuggest: shouldSuggestLabels, notebookId: suggestNotebookId, dismiss: dismissLabelSuggestion } = useAutoLabelSuggestion() const { shouldSuggest: shouldSuggestLabels, notebookId: suggestNotebookId, dismiss: dismissLabelSuggestion } = useAutoLabelSuggestion()
const [autoLabelOpen, setAutoLabelOpen] = useState(false) const [autoLabelOpen, setAutoLabelOpen] = useState(false)
const [summaryDialogOpen, setSummaryDialogOpen] = useState(false)
useEffect(() => { useEffect(() => {
if (shouldSuggestLabels && suggestNotebookId) { if (shouldSuggestLabels && suggestNotebookId) {
@@ -263,9 +269,12 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
: await getAllNotes(false, notebook || undefined) : await getAllNotes(false, notebook || undefined)
const sharedOnly = searchParams.get('shared') === '1' const sharedOnly = searchParams.get('shared') === '1'
const remindersOnly = searchParams.get('reminders') === '1'
if (sharedOnly) { if (sharedOnly) {
allNotes = allNotes.filter((note: any) => note._isShared) allNotes = allNotes.filter((note: any) => note._isShared)
} else if (remindersOnly) {
allNotes = allNotes.filter((note: any) => note.reminder !== null)
} else if (!notebook && !search) { } else if (!notebook && !search) {
allNotes = allNotes.filter((note: any) => !note.notebookId && !note._isShared) allNotes = allNotes.filter((note: any) => !note.notebookId && !note._isShared)
} }
@@ -300,10 +309,13 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
} else { } else {
let filtered = initialNotes let filtered = initialNotes
const sharedOnly = searchParams.get('shared') === '1' const sharedOnly = searchParams.get('shared') === '1'
const remindersOnly = searchParams.get('reminders') === '1'
if (notebook) { if (notebook) {
filtered = initialNotes.filter((n: any) => n.notebookId === notebook && !n._isShared) filtered = initialNotes.filter((n: any) => n.notebookId === notebook && !n._isShared)
} else if (sharedOnly) { } else if (sharedOnly) {
filtered = initialNotes.filter((n: any) => n._isShared) filtered = initialNotes.filter((n: any) => n._isShared)
} else if (remindersOnly) {
filtered = initialNotes.filter((n: any) => n.reminder !== null)
} else { } else {
filtered = initialNotes.filter((n: any) => !n.notebookId && !n._isShared) filtered = initialNotes.filter((n: any) => !n.notebookId && !n._isShared)
} }
@@ -384,14 +396,20 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
fullPage fullPage
/> />
) : ( ) : (
<div className="flex-1 overflow-y-auto min-h-0"> <div className="flex-1 overflow-y-auto min-h-0 bg-memento-paper flex flex-col">
<div className={cn( <div className={cn(
'px-12 pt-12 pb-8 flex flex-col gap-6', 'px-12 pt-12 pb-8 flex flex-col gap-6',
isEditorialMode ? 'sticky top-0 bg-background/90 backdrop-blur-md z-30' : '' isEditorialMode ? 'sticky top-0 bg-memento-paper/90 backdrop-blur-md z-30' : ''
)}> )}>
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<h1 className="font-memento-serif text-4xl font-medium tracking-tight text-foreground leading-tight pr-12"> <h1 className="font-memento-serif text-4xl font-medium tracking-tight text-foreground leading-tight pr-12">
{currentNotebook ? currentNotebook.name : (searchParams.get('shared') === '1' ? 'Partagées avec moi' : t('notes.title'))} {currentNotebook
? currentNotebook.name
: searchParams.get('shared') === '1'
? (t('sidebar.sharedWithMe') || 'Partagées avec moi')
: searchParams.get('reminders') === '1'
? (t('sidebar.reminders') || 'Rappels')
: t('notes.title')}
</h1> </h1>
</div> </div>
@@ -477,10 +495,25 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
className="flex items-center gap-2 text-[13px] text-foreground font-medium hover:opacity-70 transition-opacity" className="flex items-center gap-2 text-[13px] text-foreground font-medium hover:opacity-70 transition-opacity"
> >
<Sparkles size={16} /> <Sparkles size={16} />
<span>Réorganiser les notes</span> <span>{t('notes.reorganize') || 'Réorganiser les notes'}</span>
</button> </button>
)} )}
</div> </div>
<div className="flex items-center gap-6">
{searchParams.get('notebook') && (
<button
onClick={() => setSummaryDialogOpen(true)}
disabled={!initialSettings.aiAssistantEnabled}
className={cn(
"flex items-center gap-2 text-[13px] font-medium transition-opacity",
initialSettings.aiAssistantEnabled ? "text-foreground hover:opacity-70" : "text-muted-foreground opacity-50 cursor-not-allowed"
)}
title={initialSettings.aiAssistantEnabled ? t('notebook.summary') : "Activez l'Assistant IA dans les paramètres pour résumer"}
>
<FileText size={16} />
<span>{t('notebook.summary') || 'Summarize'}</span>
</button>
)}
<button <button
onClick={() => setSortOrder(s => s === 'newest' ? 'oldest' : s === 'oldest' ? 'alpha' : 'newest')} onClick={() => setSortOrder(s => s === 'newest' ? 'oldest' : s === 'oldest' ? 'alpha' : 'newest')}
className="flex items-center gap-2 text-[13px] text-foreground font-medium hover:opacity-70 transition-opacity" className="flex items-center gap-2 text-[13px] text-foreground font-medium hover:opacity-70 transition-opacity"
@@ -490,6 +523,7 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
</button> </button>
</div> </div>
</div> </div>
</div>
<div className="px-12 flex-1 pb-20"> <div className="px-12 flex-1 pb-20">
{isLoading ? ( {isLoading ? (
@@ -595,6 +629,15 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
onEnableHistory={async () => { if (historyNote) await handleEnableHistory(historyNote.id) }} onEnableHistory={async () => { if (historyNote) await handleEnableHistory(historyNote.id) }}
onRestored={handleHistoryRestored} onRestored={handleHistoryRestored}
/> />
{searchParams.get('notebook') && (
<NotebookSummaryDialog
open={summaryDialogOpen}
onOpenChange={setSummaryDialogOpen}
notebookId={searchParams.get('notebook')}
notebookName={currentNotebook?.name}
/>
)}
</div> </div>
) )
} }

View File

@@ -46,8 +46,8 @@ export function LabelBadge({
)} )}
onClick={onClick} onClick={onClick}
> >
{isAI && <Sparkles className="h-3 w-3 text-sky-500 dark:text-sky-400" />} {isAI && <Sparkles className="h-3 w-3 text-[#75B2D6]" />}
{label} <span className="truncate">{label}</span>
{onRemove && ( {onRemove && (
<button <button
onClick={(e) => { onClick={(e) => {
@@ -60,9 +60,9 @@ export function LabelBadge({
</button> </button>
)} )}
{isAI && ( {isAI && (
<span className="relative flex h-1.5 w-1.5 ml-0.5"> <span className="relative flex h-1.5 w-1.5 ml-1">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-sky-400 opacity-75"></span> <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[#75B2D6] opacity-75"></span>
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-sky-500"></span> <span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-[#75B2D6]"></span>
</span> </span>
)} )}
</Badge> </Badge>

View File

@@ -120,7 +120,7 @@ export function McpSettingsPanel({ initialKeys, serverStatus }: McpSettingsPanel
{/* Section 1: What is MCP */} {/* Section 1: What is MCP */}
<div className="bg-card rounded-lg border border-border shadow-sm overflow-hidden break-inside-avoid"> <div className="bg-card rounded-lg border border-border shadow-sm overflow-hidden break-inside-avoid">
<div className="flex items-center gap-3 p-6 border-b border-border"> <div className="flex items-center gap-3 p-6 border-b border-border">
<div className="w-10 h-10 rounded-full bg-blue-500/10 flex items-center justify-center text-blue-500 shrink-0"> <div className="w-10 h-10 rounded-full bg-zinc-500/10 flex items-center justify-center text-zinc-500 shrink-0">
<Info className="h-5 w-5" /> <Info className="h-5 w-5" />
</div> </div>
<div> <div>
@@ -135,7 +135,7 @@ export function McpSettingsPanel({ initialKeys, serverStatus }: McpSettingsPanel
href="https://modelcontextprotocol.io" href="https://modelcontextprotocol.io"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-sm text-blue-600 hover:underline mt-4" className="inline-flex items-center gap-1 text-sm text-zinc-600 hover:underline mt-4"
> >
{t('mcpSettings.whatIsMcp.learnMore')} {t('mcpSettings.whatIsMcp.learnMore')}
<ExternalLink className="h-3 w-3" /> <ExternalLink className="h-3 w-3" />
@@ -156,7 +156,7 @@ export function McpSettingsPanel({ initialKeys, serverStatus }: McpSettingsPanel
<div className="p-6"> <div className="p-6">
<div className="space-y-4 text-sm"> <div className="space-y-4 text-sm">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-muted-foreground">{t('mcpSettings.serverStatus.mode')}</span> <span className="text-zinc-600">{t('mcpSettings.serverStatus.mode')}</span>
<Badge variant="secondary">{serverStatus.mode.toUpperCase()}</Badge> <Badge variant="secondary">{serverStatus.mode.toUpperCase()}</Badge>
</div> </div>
{serverStatus.mode === 'sse' && serverStatus.url && ( {serverStatus.mode === 'sse' && serverStatus.url && (

View File

@@ -6,7 +6,7 @@ import { getNoteFeedImage, getNotePlainExcerpt, getNoteDisplayTitle } from '@/li
import { useLanguage } from '@/lib/i18n' import { useLanguage } from '@/lib/i18n'
import { useRefresh } from '@/lib/use-refresh' import { useRefresh } from '@/lib/use-refresh'
import { motion, AnimatePresence } from 'motion/react' import { motion, AnimatePresence } from 'motion/react'
import { ChevronRight, MoreHorizontal, Trash2, Archive, Pin, History, Pencil, Sparkles, Loader2 } from 'lucide-react' import { ChevronRight, MoreHorizontal, Trash2, Archive, Pin, History, Pencil, Sparkles, Loader2, Bell, FolderOpen, StickyNote } from 'lucide-react'
import { useSession } from 'next-auth/react' import { useSession } from 'next-auth/react'
import { getAISettings } from '@/app/actions/ai-settings' import { getAISettings } from '@/app/actions/ai-settings'
import { generateNoteIllustrationSvg } from '@/app/actions/note-illustration' import { generateNoteIllustrationSvg } from '@/app/actions/note-illustration'
@@ -16,8 +16,13 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
} from '@/components/ui/dropdown-menu' } from '@/components/ui/dropdown-menu'
import { deleteNote, toggleArchive, togglePin } from '@/app/actions/notes' import { deleteNote, toggleArchive, togglePin, updateNote } from '@/app/actions/notes'
import { ReminderDialog } from '@/components/reminder-dialog'
import { useNotebooks } from '@/context/notebooks-context'
import { toast } from 'sonner' import { toast } from 'sonner'
type NotesEditorialViewProps = { type NotesEditorialViewProps = {
@@ -43,7 +48,10 @@ function EditorialNoteMenu({ note, onOpen, onOpenHistory }: {
}) { }) {
const { t } = useLanguage() const { t } = useLanguage()
const { refreshNotes } = useRefresh() const { refreshNotes } = useRefresh()
const { notebooks } = useNotebooks()
const [, startTransition] = useTransition() const [, startTransition] = useTransition()
const [showReminder, setShowReminder] = useState(false)
const [showNotebookMenu, setShowNotebookMenu] = useState(false)
const handleDelete = (e: React.MouseEvent) => { const handleDelete = (e: React.MouseEvent) => {
e.stopPropagation() e.stopPropagation()
@@ -83,6 +91,18 @@ function EditorialNoteMenu({ note, onOpen, onOpenHistory }: {
}) })
} }
const handleMoveToNotebook = (notebookId: string | null) => {
startTransition(async () => {
try {
await updateNote(note.id, { notebookId })
refreshNotes(note?.notebookId)
toast.success(t('notebookSuggestion.movedToNotebook') || 'Note déplacée')
} catch {
toast.error(t('general.error'))
}
})
}
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild onClick={e => e.stopPropagation()}> <DropdownMenuTrigger asChild onClick={e => e.stopPropagation()}>
@@ -109,6 +129,52 @@ function EditorialNoteMenu({ note, onOpen, onOpenHistory }: {
{t('notes.history') || 'Historique'} {t('notes.history') || 'Historique'}
</DropdownMenuItem> </DropdownMenuItem>
)} )}
{/* Rappel */}
<DropdownMenuItem onClick={e => { e.stopPropagation(); setShowReminder(true) }}>
<Bell className="h-4 w-4 mr-2" />
{note.reminder ? (t('reminder.changeReminder') || 'Modifier le rappel') : (t('reminder.setReminder') || 'Définir un rappel')}
</DropdownMenuItem>
<ReminderDialog
open={showReminder}
onOpenChange={setShowReminder}
currentReminder={note.reminder ? new Date(note.reminder) : null}
onSave={(date) => {
startTransition(async () => {
await updateNote(note.id, { reminder: date })
refreshNotes(note?.notebookId)
setShowReminder(false)
})
}}
onRemove={() => {
startTransition(async () => {
await updateNote(note.id, { reminder: null })
refreshNotes(note?.notebookId)
setShowReminder(false)
})
}}
/>
{/* Déplacer vers notebook */}
<DropdownMenuSub open={showNotebookMenu} onOpenChange={setShowNotebookMenu}>
<DropdownMenuSubTrigger onClick={e => e.stopPropagation()}>
<FolderOpen className="h-4 w-4 mr-2" />
{t('notebookSuggestion.moveToNotebook') || 'Déplacer vers notebook'}
</DropdownMenuSubTrigger>
<DropdownMenuSubContent alignOffset={0} className="w-48">
<DropdownMenuItem onClick={e => { e.stopPropagation(); handleMoveToNotebook(null) }}>
<StickyNote className="h-4 w-4 mr-2" />
{t('notebookSuggestion.generalNotes') || 'Notes générales'}
</DropdownMenuItem>
{notebooks.map((nb: any) => (
<DropdownMenuItem key={nb.id} onClick={e => { e.stopPropagation(); handleMoveToNotebook(nb.id) }}>
<FolderOpen className="h-4 w-4 mr-2" />
{nb.name}
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem onClick={handleDelete} className="text-red-600 dark:text-red-400 focus:text-red-600"> <DropdownMenuItem onClick={handleDelete} className="text-red-600 dark:text-red-400 focus:text-red-600">
<Trash2 className="h-4 w-4 mr-2" /> <Trash2 className="h-4 w-4 mr-2" />

View File

@@ -48,11 +48,11 @@ interface ReminderNote {
// ── Memento brand tokens ────────────────────────────────────────────────────── // ── Memento brand tokens ──────────────────────────────────────────────────────
const C = { const C = {
blue: '#E9ECEF', blue: '#FDFDFE',
gold: '#D4A373', gold: '#D4A373',
green: '#A3B18A', green: '#A3B18A',
dark: '#1C1C1C', dark: '#1C1C1C',
beige: '#F2F0E9', beige: '#FDFDFE',
} }
export function NotificationPanel() { export function NotificationPanel() {
@@ -148,20 +148,20 @@ export function NotificationPanel() {
// ── icon bg/color per notification type ────────────────────────────────── // ── icon bg/color per notification type ──────────────────────────────────
const notifIconStyle = (type: string) => { const notifIconStyle = (type: string) => {
if (type === 'agent_success') return { bg: `${C.green}20`, color: C.green } if (type === 'agent_success') return { bg: `${C.gold}20`, color: C.gold }
if (type === 'agent_slides_ready') return { bg: `${C.blue}20`, color: C.blue } if (type === 'agent_slides_ready') return { bg: `${C.gold}20`, color: C.gold }
if (type === 'agent_canvas_ready') return { bg: `${C.blue}20`, color: C.blue } if (type === 'agent_canvas_ready') return { bg: `${C.gold}20`, color: C.gold }
if (type === 'agent_failure') return { bg: '#EF444420', color: '#EF4444' } if (type === 'agent_failure') return { bg: '#EF444420', color: '#EF4444' }
return { bg: `${C.gold}20`, color: C.gold } return { bg: `${C.green}20`, color: C.green }
} }
const notifLabelColor = (type: string) => { const notifLabelColor = (type: string) => {
if (type === 'agent_success') return C.green if (type.startsWith('agent')) {
if (type === 'agent_slides_ready') return C.blue
if (type === 'agent_canvas_ready') return C.blue
if (type === 'agent_failure') return '#EF4444' if (type === 'agent_failure') return '#EF4444'
return C.gold return C.gold
} }
return C.green
}
return ( return (
<Popover open={open} onOpenChange={setOpen}> <Popover open={open} onOpenChange={setOpen}>
@@ -173,7 +173,7 @@ export function NotificationPanel() {
{pendingCount > 0 && ( {pendingCount > 0 && (
<span <span
className="absolute -top-1 -right-1 h-4 w-4 flex items-center justify-center rounded-full text-white text-[9px] font-bold border border-white shadow-sm" className="absolute -top-1 -right-1 h-4 w-4 flex items-center justify-center rounded-full text-white text-[9px] font-bold border border-white shadow-sm"
style={{ background: C.gold }} style={{ background: C.green }}
> >
{pendingCount > 9 ? '9+' : pendingCount} {pendingCount > 9 ? '9+' : pendingCount}
</span> </span>
@@ -181,9 +181,9 @@ export function NotificationPanel() {
</button> </button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent align="end" className="w-80 p-0 rounded-2xl overflow-hidden shadow-xl border border-black/10"> <PopoverContent align="end" className="w-80 p-0 rounded-2xl overflow-hidden shadow-2xl border border-black/20">
{/* Header */} {/* Header */}
<div className="px-4 py-3 border-b flex items-center justify-between" style={{ background: `${C.beige}` }}> <div className="px-4 py-3 border-b flex items-center justify-between" style={{ background: '#FDFDFE' }}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Bell className="h-4 w-4" style={{ color: C.dark }} /> <Bell className="h-4 w-4" style={{ color: C.dark }} />
<span className="font-bold text-sm tracking-tight" style={{ color: C.dark }}> <span className="font-bold text-sm tracking-tight" style={{ color: C.dark }}>
@@ -203,7 +203,7 @@ export function NotificationPanel() {
{pendingCount > 0 && ( {pendingCount > 0 && (
<span <span
className="h-5 px-1.5 flex items-center justify-center rounded-full text-white text-[9px] font-bold" className="h-5 px-1.5 flex items-center justify-center rounded-full text-white text-[9px] font-bold"
style={{ background: C.gold }} style={{ background: C.green }}
> >
{pendingCount} {pendingCount}
</span> </span>
@@ -323,15 +323,15 @@ export function NotificationPanel() {
<button <button
onClick={() => handleToggleReminder(note.id, true)} onClick={() => handleToggleReminder(note.id, true)}
className="mt-0.5 flex-none transition-colors hover:opacity-70" className="mt-0.5 flex-none transition-colors hover:opacity-70"
style={{ color: C.gold }} style={{ color: C.green }}
title={t('reminders.markDone')} title={t('reminders.markDone')}
> >
<Circle className="w-4 h-4" /> <Circle className="w-4 h-4" />
</button> </button>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5 mb-0.5"> <div className="flex items-center gap-1.5 mb-0.5">
<AlertCircle className="w-3 h-3" style={{ color: C.gold }} /> <AlertCircle className="w-3 h-3" style={{ color: C.green }} />
<span className="text-[9px] font-bold uppercase tracking-[0.2em]" style={{ color: C.gold }}> <span className="text-[9px] font-bold uppercase tracking-[0.2em]" style={{ color: C.green }}>
{t('reminders.overdue')} {t('reminders.overdue')}
</span> </span>
</div> </div>

View File

@@ -94,6 +94,7 @@ function SidebarCarnetItem({
isDragging?: boolean isDragging?: boolean
dragHandleProps?: React.HTMLAttributes<HTMLDivElement> dragHandleProps?: React.HTMLAttributes<HTMLDivElement>
}) { }) {
const { t } = useLanguage()
return ( return (
<div className={cn('space-y-1 transition-opacity', isDragging && 'opacity-40')}> <div className={cn('space-y-1 transition-opacity', isDragging && 'opacity-40')}>
<div className="relative group/carnet"> <div className="relative group/carnet">
@@ -160,7 +161,7 @@ function SidebarCarnetItem({
/> />
))} ))}
{notes.length === 0 && ( {notes.length === 0 && (
<p className="pl-12 text-[11px] text-muted-foreground/50 py-2 italic font-light">No notes yet</p> <p className="pl-12 text-[11px] text-muted-foreground/50 py-2 italic font-light">{t('common.noResults')}</p>
)} )}
</motion.div> </motion.div>
)} )}
@@ -234,7 +235,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
const notes = await getAllNotes(false, nb.id) const notes = await getAllNotes(false, nb.id)
const mapped = notes.map((n: Note) => ({ const mapped = notes.map((n: Note) => ({
id: n.id, id: n.id,
title: getNoteDisplayTitle(n, t('notes.untitled') || 'Untitled'), title: getNoteDisplayTitle(n, t('notes.untitled')),
})) }))
return [nb.id, mapped] as const return [nb.id, mapped] as const
}) })
@@ -325,9 +326,9 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
} }
const sortLabels: Record<SortOrder, string> = { const sortLabels: Record<SortOrder, string> = {
newest: t('sidebar.sortNewest') || 'Newest first', newest: t('sidebar.sortNewest'),
oldest: t('sidebar.sortOldest') || 'Oldest first', oldest: t('sidebar.sortOldest'),
alpha: t('sidebar.sortAlpha') || 'A → Z', alpha: t('sidebar.sortAlpha'),
} }
return ( return (
@@ -335,7 +336,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
<aside <aside
className={cn( className={cn(
'hidden h-full min-h-0 w-72 shrink-0 flex-col lg:w-80 md:flex', 'hidden h-full min-h-0 w-72 shrink-0 flex-col lg:w-80 md:flex',
'border-e border-border/40 bg-[#F6F4F0] backdrop-blur-md sidebar-shadow dark:border-white/6 dark:bg-[#252525] dark:backdrop-blur-none', 'border-e border-border/40 bg-memento-sidebar backdrop-blur-md sidebar-shadow dark:border-white/6 dark:bg-[#252525] dark:backdrop-blur-none',
className className
)} )}
> >
@@ -346,13 +347,13 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
<button <button
type="button" type="button"
className="shrink-0 rounded-full outline-none ring-offset-background transition-shadow hover:ring-2 hover:ring-primary/30 focus-visible:ring-2 focus-visible:ring-ring" className="shrink-0 rounded-full outline-none ring-offset-background transition-shadow hover:ring-2 hover:ring-primary/30 focus-visible:ring-2 focus-visible:ring-ring"
aria-label={t('sidebar.accountMenu') || 'Menu du compte'} aria-label={t('sidebar.accountMenu')}
> >
<div className="w-10 h-10 rounded-full bg-[#E9ECEF] border border-black/10 flex items-center justify-center text-foreground font-memento-serif text-lg shadow-sm"> <div className="w-10 h-10 rounded-full bg-secondary border border-black/10 flex items-center justify-center text-foreground font-memento-serif text-lg shadow-sm">
{user?.image ? ( {user?.image ? (
<Avatar className="size-10 ring-1 ring-border/60"> <Avatar className="size-10 ring-1 ring-border/60">
<AvatarImage src={user.image} alt="" /> <AvatarImage src={user.image} alt="" />
<AvatarFallback className="bg-[#E9ECEF] text-sm font-semibold text-[#1C1C1C]/60">{initial}</AvatarFallback> <AvatarFallback className="bg-secondary text-sm font-semibold text-[#1C1C1C]/60">{initial}</AvatarFallback>
</Avatar> </Avatar>
) : ( ) : (
<span>{initial}</span> <span>{initial}</span>
@@ -399,14 +400,14 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
<button <button
onClick={() => { setActiveView('notebooks'); if (pathname !== '/') router.push('/') }} onClick={() => { setActiveView('notebooks'); if (pathname !== '/') router.push('/') }}
className={cn('p-1.5 rounded-full transition-all', activeView === 'notebooks' ? 'bg-foreground text-background' : 'text-muted-foreground hover:text-foreground')} className={cn('p-1.5 rounded-full transition-all', activeView === 'notebooks' ? 'bg-foreground text-background' : 'text-muted-foreground hover:text-foreground')}
title={t('nav.notebooks') || 'Notebooks'} title={t('nav.notebooks')}
> >
<BookOpen size={14} /> <BookOpen size={14} />
</button> </button>
<button <button
onClick={() => { setActiveView('agents'); router.push('/agents') }} onClick={() => { setActiveView('agents'); router.push('/agents') }}
className={cn('p-1.5 rounded-full transition-all', activeView === 'agents' ? 'bg-foreground text-background' : 'text-muted-foreground hover:text-foreground')} className={cn('p-1.5 rounded-full transition-all', activeView === 'agents' ? 'bg-foreground text-background' : 'text-muted-foreground hover:text-foreground')}
title={t('nav.agents') || 'Agents'} title={t('nav.agents')}
> >
<Bot size={14} /> <Bot size={14} />
</button> </button>
@@ -429,13 +430,13 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
{/* Section header with sort button */} {/* Section header with sort button */}
<div className="flex items-center justify-between px-4 mb-3"> <div className="flex items-center justify-between px-4 mb-3">
<p className="text-[10px] font-bold text-muted-foreground tracking-widest uppercase"> <p className="text-[10px] font-bold text-muted-foreground tracking-widest uppercase">
{t('nav.notebooks') || 'Notebooks'} {t('nav.notebooks')}
</p> </p>
<div className="relative"> <div className="relative">
<button <button
onClick={() => setShowSortMenu(s => !s)} onClick={() => setShowSortMenu(s => !s)}
className="p-1 text-muted-foreground hover:text-foreground transition-colors rounded" className="p-1 text-muted-foreground hover:text-foreground transition-colors rounded"
title={t('sidebar.sortOrder') || 'Sort order'} title={t('sidebar.sortOrder')}
> >
<ArrowUpDown size={12} /> <ArrowUpDown size={12} />
</button> </button>
@@ -484,18 +485,22 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
'text-[13px] font-medium truncate', 'text-[13px] font-medium truncate',
isInboxActive ? 'text-foreground' : 'text-muted-foreground' isInboxActive ? 'text-foreground' : 'text-muted-foreground'
)}> )}>
{t('sidebar.inbox') || 'Inbox'} {t('sidebar.inbox')}
</span> </span>
</button> </button>
{/* Partagées avec moi */} <button
<Link onClick={() => {
href="/shared" const params = new URLSearchParams()
className={cn('sidebar-inbox-item', pathname === '/shared' && 'active')} params.set('shared', '1')
params.set('forceList', '1')
router.push(`/?${params.toString()}`)
}}
className={cn('sidebar-inbox-item', searchParams.get('shared') === '1' && pathname === '/' && 'active')}
> >
<div className={cn( <div className={cn(
'w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium border shrink-0', 'w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium border shrink-0',
pathname === '/shared' searchParams.get('shared') === '1' && pathname === '/'
? 'bg-foreground text-background border-foreground' ? 'bg-foreground text-background border-foreground'
: 'bg-white/60 text-foreground border-border' : 'bg-white/60 text-foreground border-border'
)}> )}>
@@ -503,20 +508,24 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
</div> </div>
<span className={cn( <span className={cn(
'text-[13px] font-medium truncate', 'text-[13px] font-medium truncate',
pathname === '/shared' ? 'text-foreground' : 'text-muted-foreground' searchParams.get('shared') === '1' && pathname === '/' ? 'text-foreground' : 'text-muted-foreground'
)}> )}>
{t('sidebar.sharedWithMe') || 'Partagées avec moi'} {t('sidebar.sharedWithMe')}
</span> </span>
</Link> </button>
{/* Rappels */} <button
<Link onClick={() => {
href="/reminders" const params = new URLSearchParams()
className={cn('sidebar-inbox-item', pathname === '/reminders' && 'active')} params.set('reminders', '1')
params.set('forceList', '1')
router.push(`/?${params.toString()}`)
}}
className={cn('sidebar-inbox-item', searchParams.get('reminders') === '1' && pathname === '/' && 'active')}
> >
<div className={cn( <div className={cn(
'w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium border shrink-0', 'w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium border shrink-0',
pathname === '/reminders' searchParams.get('reminders') === '1' && pathname === '/'
? 'bg-foreground text-background border-foreground' ? 'bg-foreground text-background border-foreground'
: 'bg-white/60 text-foreground border-border' : 'bg-white/60 text-foreground border-border'
)}> )}>
@@ -524,11 +533,11 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
</div> </div>
<span className={cn( <span className={cn(
'text-[13px] font-medium truncate', 'text-[13px] font-medium truncate',
pathname === '/reminders' ? 'text-foreground' : 'text-muted-foreground' searchParams.get('reminders') === '1' && pathname === '/' ? 'text-foreground' : 'text-muted-foreground'
)}> )}>
{t('sidebar.reminders') || 'Rappels'} {t('sidebar.reminders')}
</span> </span>
</Link> </button>
{/* Divider */} {/* Divider */}
<div className="mx-4 my-3 h-px bg-border/40" /> <div className="mx-4 my-3 h-px bg-border/40" />
@@ -583,7 +592,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
className="w-full mt-4 flex items-center gap-3 px-4 py-2 text-[13px] text-muted-foreground hover:text-foreground transition-colors font-medium rounded-lg hover:bg-white/40" className="w-full mt-4 flex items-center gap-3 px-4 py-2 text-[13px] text-muted-foreground hover:text-foreground transition-colors font-medium rounded-lg hover:bg-white/40"
> >
<Plus size={16} /> <Plus size={16} />
<span>{t('notebooks.create') || 'New Carnet'}</span> <span>{t('notebook.create')}</span>
</button> </button>
</div> </div>
</motion.div> </motion.div>
@@ -596,12 +605,12 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
> >
<p className="text-[10px] font-bold text-muted-foreground tracking-widest uppercase mb-4 px-4"> <p className="text-[10px] font-bold text-muted-foreground tracking-widest uppercase mb-4 px-4">
{t('agents.intelligenceOS') || 'Intelligence OS'} {t('agents.intelligenceOS')}
</p> </p>
<div className="space-y-1"> <div className="space-y-1">
{[ {[
{ id: 'agents', href: '/agents', label: t('agents.myAgents') || 'Mes Agents', icon: Bot }, { id: 'agents', href: '/agents', label: t('agents.myAgents'), icon: Bot },
{ id: 'lab', href: '/lab', label: t('nav.lab') || 'Le Lab AI', icon: FlaskConical }, { id: 'lab', href: '/lab', label: t('nav.lab'), icon: FlaskConical },
].map(item => { ].map(item => {
const isActive = pathname.startsWith(item.href) const isActive = pathname.startsWith(item.href)
return ( return (
@@ -638,21 +647,21 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
className="flex items-center gap-3 px-4 py-2 text-[13px] text-[#1C1C1C]/60 hover:text-[#1C1C1C] transition-colors font-medium rounded-lg hover:bg-white/30" className="flex items-center gap-3 px-4 py-2 text-[13px] text-[#1C1C1C]/60 hover:text-[#1C1C1C] transition-colors font-medium rounded-lg hover:bg-white/30"
> >
<Archive size={16} /> <Archive size={16} />
<span>{t('sidebar.archive') || 'Archives'}</span> <span>{t('sidebar.archive')}</span>
</Link> </Link>
<Link <Link
href="/trash" href="/trash"
className="flex items-center gap-3 px-4 py-2 text-[13px] text-[#1C1C1C]/60 hover:text-[#1C1C1C] transition-colors font-medium rounded-lg hover:bg-white/30" className="flex items-center gap-3 px-4 py-2 text-[13px] text-[#1C1C1C]/60 hover:text-[#1C1C1C] transition-colors font-medium rounded-lg hover:bg-white/30"
> >
<Trash2 size={16} /> <Trash2 size={16} />
<span>{t('sidebar.trash') || 'Corbeille'}</span> <span>{t('sidebar.trash')}</span>
</Link> </Link>
<Link <Link
href="/settings" href="/settings"
className="flex items-center gap-3 px-4 py-2 text-[13px] text-[#1C1C1C]/60 hover:text-[#1C1C1C] transition-colors font-medium rounded-lg hover:bg-white/30" className="flex items-center gap-3 px-4 py-2 text-[13px] text-[#1C1C1C]/60 hover:text-[#1C1C1C] transition-colors font-medium rounded-lg hover:bg-white/30"
> >
<Settings size={16} /> <Settings size={16} />
<span>{t('nav.settings') || 'Paramètres'}</span> <span>{t('nav.settings')}</span>
</Link> </Link>
</div> </div>
</aside> </aside>

View File

@@ -29,7 +29,7 @@ export function Toaster() {
success: 'border-l-4 border-l-emerald-400', success: 'border-l-4 border-l-emerald-400',
error: 'border-l-4 border-l-red-400', error: 'border-l-4 border-l-red-400',
warning: 'border-l-4 border-l-amber-400', warning: 'border-l-4 border-l-amber-400',
info: 'border-l-4 border-l-sky-400', info: 'border-l-4 border-l-zinc-400',
}, },
}} }}
/> />

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

View File

@@ -24,37 +24,40 @@ function updateDocumentDirection(lang: SupportedLanguage) {
document.documentElement.dir = RTL_LANGUAGES.includes(lang) ? 'rtl' : 'ltr' document.documentElement.dir = RTL_LANGUAGES.includes(lang) ? 'rtl' : 'ltr'
} }
/**
* Resolve the actual language to use:
* 1. If localStorage has a saved preference, use that (client only)
* 2. Otherwise fall back to the server-detected initialLanguage
*/
function resolveLanguage(fallback: SupportedLanguage): SupportedLanguage {
if (typeof window !== 'undefined') {
try {
const saved = localStorage.getItem('user-language') as SupportedLanguage
if (saved && SUPPORTED_LANGS.includes(saved)) return saved
} catch {}
}
return fallback
}
export function LanguageProvider({ children, initialLanguage = 'en', initialTranslations }: { export function LanguageProvider({ children, initialLanguage = 'en', initialTranslations }: {
children: ReactNode children: ReactNode
initialLanguage?: SupportedLanguage initialLanguage?: SupportedLanguage
initialTranslations?: Translations initialTranslations?: Translations
}) { }) {
// Resolve language synchronously from localStorage BEFORE any effect runs. // Use server-provided initialLanguage and translations for the first render
// This prevents the flash where initialLanguage ('en') overrides RTL. // to ensure client hydration matches server HTML exactly.
const [language, setLanguageState] = useState<SupportedLanguage>(() => resolveLanguage(initialLanguage)) const [language, setLanguageState] = useState<SupportedLanguage>(initialLanguage)
// Start with server-provided translations or fallback to English ONLY if it's the requested language
const [translations, setTranslations] = useState<Translations>(() => {
if (initialTranslations) return initialTranslations
return enTranslations as unknown as Translations
})
// Start with server-provided translations or English fallback
const [translations, setTranslations] = useState<Translations>(
(initialTranslations || enTranslations) as unknown as Translations
)
const cacheRef = useRef<Map<SupportedLanguage, Translations>>(new Map()) const cacheRef = useRef<Map<SupportedLanguage, Translations>>(new Map())
// Initialize cache with initial translations to prevent redundant loading
if (initialTranslations && !cacheRef.current.has(initialLanguage)) {
cacheRef.current.set(initialLanguage, initialTranslations)
}
const isFirstRender = useRef(true) const isFirstRender = useRef(true)
// Load saved preference from localStorage AFTER hydration
useEffect(() => {
const saved = localStorage.getItem('user-language') as SupportedLanguage
if (saved && SUPPORTED_LANGS.includes(saved) && saved !== initialLanguage) {
setLanguageState(saved)
}
}, [initialLanguage])
// Load translations when language changes (with caching) // Load translations when language changes (with caching)
// On first render, skip updateDocumentDirection since the inline script already set it. // On first render, skip updateDocumentDirection since the inline script already set it.
useEffect(() => { useEffect(() => {
@@ -79,6 +82,7 @@ export function LanguageProvider({ children, initialLanguage = 'en', initialTran
const setLanguage = useCallback((lang: SupportedLanguage) => { const setLanguage = useCallback((lang: SupportedLanguage) => {
setLanguageState(lang) setLanguageState(lang)
localStorage.setItem('user-language', lang) localStorage.setItem('user-language', lang)
document.cookie = `user-language=${lang};path=/;max-age=${60 * 60 * 24 * 365};samesite=lax`
updateDocumentDirection(lang) updateDocumentDirection(lang)
}, []) }, [])

View File

@@ -1,6 +1,7 @@
/** /**
* Detect user's preferred language. * Detect user's preferred language.
* Priority: * Priority:
* 0. user-language cookie (explicit user choice, synced with localStorage)
* 1. Most common language among user's notes (DB GROUP BY) * 1. Most common language among user's notes (DB GROUP BY)
* 2. Browser language hint (passed from server component via Accept-Language) * 2. Browser language hint (passed from server component via Accept-Language)
* 3. Default: 'en' * 3. Default: 'en'
@@ -8,7 +9,7 @@
import { auth } from '@/auth' import { auth } from '@/auth'
import { prisma } from '@/lib/prisma' import { prisma } from '@/lib/prisma'
import { unstable_cache } from 'next/cache' import { unstable_cache, cookies } from 'next/cache'
import { SupportedLanguage } from './load-translations' import { SupportedLanguage } from './load-translations'
const SUPPORTED_LANGUAGES = new Set(['en', 'fr', 'es', 'de', 'fa', 'it', 'pt', 'ru', 'zh', 'ja', 'ko', 'ar', 'hi', 'nl', 'pl']) const SUPPORTED_LANGUAGES = new Set(['en', 'fr', 'es', 'de', 'fa', 'it', 'pt', 'ru', 'zh', 'ja', 'ko', 'ar', 'hi', 'nl', 'pl'])
@@ -79,6 +80,16 @@ const getCachedUserLanguage = unstable_cache(
* Should be passed from server components that have access to headers(). * Should be passed from server components that have access to headers().
*/ */
export async function detectUserLanguage(browserLanguageHint?: SupportedLanguage | null): Promise<SupportedLanguage> { export async function detectUserLanguage(browserLanguageHint?: SupportedLanguage | null): Promise<SupportedLanguage> {
// 0. Cookie has highest priority — set by the client when user picks a language.
// This guarantees server and client agree on the language (no hydration mismatch).
try {
const cookieStore = await cookies()
const cookieLang = cookieStore.get('user-language')?.value
if (cookieLang && SUPPORTED_LANGUAGES.has(cookieLang)) {
return cookieLang as SupportedLanguage
}
} catch {}
const session = await auth() const session = await auth()
if (!session?.user?.id) { if (!session?.user?.id) {

View File

@@ -42,11 +42,21 @@
"noLabelsInNotebook": "No labels in this notebook yet", "noLabelsInNotebook": "No labels in this notebook yet",
"archive": "Archive", "archive": "Archive",
"trash": "Trash", "trash": "Trash",
"clearFilter": "Remove filter" "clearFilter": "Remove filter",
"inbox": "Inbox",
"sharedWithMe": "Shared with me",
"sortNewest": "Newest first",
"sortOldest": "Oldest first",
"sortAlpha": "A → Z",
"accountMenu": "Account menu",
"profile": "Profile",
"signOut": "Sign out",
"sortOrder": "Sort order"
}, },
"notes": { "notes": {
"title": "Notes", "title": "Notes",
"newNote": "New note", "newNote": "New note",
"reorganize": "Reorganize notes",
"untitled": "Untitled", "untitled": "Untitled",
"placeholder": "Take a note...", "placeholder": "Take a note...",
"markdownPlaceholder": "Take a note... (Markdown supported)", "markdownPlaceholder": "Take a note... (Markdown supported)",

View File

@@ -42,11 +42,21 @@
"noLabelsInNotebook": "Aucune étiquette dans ce carnet", "noLabelsInNotebook": "Aucune étiquette dans ce carnet",
"archive": "Archives", "archive": "Archives",
"trash": "Corbeille", "trash": "Corbeille",
"clearFilter": "Retirer le filtre" "clearFilter": "Retirer le filtre",
"inbox": "Boîte de réception",
"sharedWithMe": "Partagées avec moi",
"sortNewest": "Plus récentes",
"sortOldest": "Plus anciennes",
"sortAlpha": "A → Z",
"accountMenu": "Menu du compte",
"profile": "Profil",
"signOut": "Se déconnecter",
"sortOrder": "Ordre de tri"
}, },
"notes": { "notes": {
"title": "Notes", "title": "Notes",
"newNote": "Nouvelle note", "newNote": "Nouvelle note",
"reorganize": "Réorganiser les notes",
"untitled": "Sans titre", "untitled": "Sans titre",
"placeholder": "Prenez une note...", "placeholder": "Prenez une note...",
"markdownPlaceholder": "Prenez une note... (Markdown supporté)", "markdownPlaceholder": "Prenez une note... (Markdown supporté)",

View File

@@ -18,6 +18,21 @@ const nextConfig: NextConfig = {
] ]
}, },
async redirects() {
return [
{
source: '/shared',
destination: '/?shared=1',
permanent: false,
},
{
source: '/reminders',
destination: '/?reminders=1',
permanent: false,
},
]
},
// Image optimization (enabled for better performance) // Image optimization (enabled for better performance)
images: { images: {
formats: ['image/avif', 'image/webp'], formats: ['image/avif', 'image/webp'],