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
Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 1m50s
This commit is contained in:
@@ -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="flex min-h-0 flex-1 flex-col overflow-y-auto scroll-smooth bg-background">
|
<main className="flex min-h-0 flex-1 flex-col overflow-y-auto scroll-smooth bg-memento-paper">
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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} />
|
|
||||||
}
|
|
||||||
@@ -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')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -342,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;
|
||||||
@@ -365,7 +367,7 @@ html.dark .memento-active-nav {
|
|||||||
--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;
|
||||||
@@ -403,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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 (
|
||||||
<LanguageProvider
|
<QueryProvider>
|
||||||
initialLanguage={initialLanguage as any}
|
<NoteRefreshProvider>
|
||||||
initialTranslations={initialTranslations}
|
<LanguageProvider
|
||||||
>
|
initialLanguage={initialLanguage as any}
|
||||||
{children}
|
initialTranslations={initialTranslations}
|
||||||
</LanguageProvider>
|
>
|
||||||
|
{children}
|
||||||
|
</LanguageProvider>
|
||||||
|
</NoteRefreshProvider>
|
||||||
|
</QueryProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,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-[#FDFDFE] 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 */}
|
||||||
@@ -207,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')}
|
||||||
@@ -216,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')}
|
||||||
@@ -225,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')}
|
||||||
@@ -239,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>
|
||||||
@@ -260,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-zinc-100 border-zinc-200 text-zinc-600'
|
? '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} />
|
||||||
@@ -281,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>
|
||||||
@@ -295,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" />
|
||||||
@@ -323,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) => ({
|
||||||
@@ -350,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-[#FDFDFE] 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>
|
||||||
@@ -392,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-[#FDFDFE] 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" />
|
||||||
@@ -405,7 +405,7 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Text Input */}
|
{/* Text Input */}
|
||||||
<div className="relative bg-[#FDFDFE] 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')}
|
||||||
@@ -440,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()}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -188,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}
|
||||||
@@ -202,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}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -113,9 +113,9 @@ export function ChatInput({ onSend, isLoading, onStop, notebooks, currentNoteboo
|
|||||||
onClick={onToggleWebSearch}
|
onClick={onToggleWebSearch}
|
||||||
className={cn(
|
className={cn(
|
||||||
"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" />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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')
|
||||||
@@ -482,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-[#FDFDFE] 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]',
|
||||||
@@ -497,25 +497,25 @@ 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-[#1C1C1C] 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-memento-accent" />
|
<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 group"
|
className="p-1.5 hover:bg-muted/60 rounded-full transition-colors text-foreground/40 group"
|
||||||
>
|
>
|
||||||
<div className="relative w-5 h-5 flex items-center justify-center">
|
<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" />
|
<ChevronRight size={20} className="transition-all duration-200 group-hover:opacity-0 group-hover:scale-0" />
|
||||||
@@ -528,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
|
||||||
@@ -537,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-memento-accent" />
|
<motion.div layoutId="activeTab" className="absolute bottom-0 left-0 right-0 h-[2px] bg-memento-blue" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -550,40 +550,40 @@ 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-[#FDFDFE]/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-[#75B2D6]">{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-[#FDFDFE] 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-[#FDFDFE]/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-memento-blue">
|
<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-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>
|
<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>
|
||||||
@@ -601,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-memento-blue/60" />
|
<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) => {
|
||||||
@@ -614,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-[#FDFDFE]' : '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-[#FDFDFE] 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>
|
||||||
@@ -630,7 +630,7 @@ 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-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 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>
|
||||||
@@ -642,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-[#FDFDFE]">
|
<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 => (
|
||||||
@@ -660,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
|
||||||
@@ -673,10 +673,10 @@ export function ContextualAIChat({
|
|||||||
'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-memento-blue/10 border-memento-blue text-memento-blue'
|
? '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-memento-blue' : '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>
|
||||||
)
|
)
|
||||||
@@ -688,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-memento-blue 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)}
|
||||||
@@ -698,7 +698,7 @@ 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-memento-blue bg-memento-blue/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} />
|
||||||
@@ -708,7 +708,7 @@ export function ContextualAIChat({
|
|||||||
</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>
|
||||||
)}
|
)}
|
||||||
@@ -724,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>
|
||||||
|
|
||||||
@@ -744,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-memento-blue bg-memento-blue/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-[#FDFDFE] shadow-sm", loading && "animate-pulse", isActive && "bg-memento-blue 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-memento-blue" : "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>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -762,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
|
||||||
@@ -772,7 +772,7 @@ export function ContextualAIChat({
|
|||||||
"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-memento-blue border-memento-blue text-white shadow-md shadow-memento-blue/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}
|
||||||
@@ -781,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}
|
||||||
@@ -790,7 +790,7 @@ 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>
|
||||||
|
|
||||||
@@ -810,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>
|
||||||
)}
|
)}
|
||||||
@@ -831,34 +831,34 @@ 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-memento-blue/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-memento-blue" />
|
<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-memento-blue"><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-memento-blue/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>
|
||||||
@@ -883,7 +883,7 @@ export function ContextualAIChat({
|
|||||||
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} />
|
||||||
@@ -913,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-[#75B2D6] 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
|
||||||
@@ -924,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>
|
||||||
@@ -950,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>
|
||||||
@@ -962,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>
|
||||||
|
|
||||||
@@ -970,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} />
|
||||||
@@ -987,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} />}
|
||||||
@@ -1017,27 +1017,27 @@ 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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
@@ -481,13 +499,29 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div className="flex items-center gap-6">
|
||||||
onClick={() => setSortOrder(s => s === 'newest' ? 'oldest' : s === 'oldest' ? 'alpha' : 'newest')}
|
{searchParams.get('notebook') && (
|
||||||
className="flex items-center gap-2 text-[13px] text-foreground font-medium hover:opacity-70 transition-opacity"
|
<button
|
||||||
>
|
onClick={() => setSummaryDialogOpen(true)}
|
||||||
<ArrowUpDown size={16} />
|
disabled={!initialSettings.aiAssistantEnabled}
|
||||||
<span>{sortLabels[sortOrder]}</span>
|
className={cn(
|
||||||
</button>
|
"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
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<ArrowUpDown size={16} />
|
||||||
|
<span>{sortLabels[sortOrder]}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -31,15 +31,23 @@ export function LanguageProvider({ children, initialLanguage = 'en', initialTran
|
|||||||
initialLanguage?: SupportedLanguage
|
initialLanguage?: SupportedLanguage
|
||||||
initialTranslations?: Translations
|
initialTranslations?: Translations
|
||||||
}) {
|
}) {
|
||||||
// Use server-provided initialLanguage for the first render to prevent hydration mismatch.
|
// Use server-provided initialLanguage and translations for the first render
|
||||||
|
// to ensure client hydration matches server HTML exactly.
|
||||||
const [language, setLanguageState] = useState<SupportedLanguage>(initialLanguage)
|
const [language, setLanguageState] = useState<SupportedLanguage>(initialLanguage)
|
||||||
|
|
||||||
// Start with server-provided translations or English fallback
|
// Start with server-provided translations or fallback to English ONLY if it's the requested language
|
||||||
const [translations, setTranslations] = useState<Translations>(
|
const [translations, setTranslations] = useState<Translations>(() => {
|
||||||
(initialTranslations || enTranslations) as unknown as Translations
|
if (initialTranslations) return initialTranslations
|
||||||
)
|
return 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
|
// Load saved preference from localStorage AFTER hydration
|
||||||
@@ -74,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)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -17,6 +17,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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user