feat: design system overhaul — sidebar, AI chats, settings, brainstorm, color cleanup
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
- Sidebar: dynamic brand-accent colors, brainstorm section restyled - AI chat general: popup panel with expand/collapse, hides when contextual AI open - AI chat contextual: tabs reordered (Actions first), X close button, height fix - Settings: all tabs restyled, 6 new color presets (sage, terracotta, iron, etc.) - Global color cleanup: emerald/orange hardcoded → brand-accent dynamic - Brainstorm page: orange → brand-accent throughout - PageEntry animation component added to key pages - Floating AI button: bg-brand-accent instead of hardcoded black - i18n: all 15 locales updated with new AI/billing keys - Billing: freemium quota tracking, BYOK, stripe subscription scaffolding - Admin: integrated into new design - AGENTS.md + CLAUDE.md project rules added
This commit is contained in:
@@ -25,16 +25,16 @@ import { toast } from 'sonner'
|
||||
|
||||
// ── Custom Toast Helper ──────────────────────────────────────────────────────
|
||||
const mToast = {
|
||||
success: (msg: string, options?: any) => toast.success(msg, {
|
||||
...options,
|
||||
success: (msg: string, options?: any) => toast.success(msg, {
|
||||
...options,
|
||||
className: 'memento-toast memento-toast-success',
|
||||
}),
|
||||
error: (msg: string, options?: any) => toast.error(msg, {
|
||||
...options,
|
||||
error: (msg: string, options?: any) => toast.error(msg, {
|
||||
...options,
|
||||
className: 'memento-toast memento-toast-error',
|
||||
}),
|
||||
loading: (msg: string, options?: any) => toast.loading(msg, {
|
||||
...options,
|
||||
loading: (msg: string, options?: any) => toast.loading(msg, {
|
||||
...options,
|
||||
className: 'memento-toast memento-toast-info',
|
||||
}),
|
||||
}
|
||||
@@ -66,12 +66,12 @@ function getMessageContent(msg: UIMessage): string {
|
||||
|
||||
// ── Constants ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const TONES = [
|
||||
{ id: 'professional', label: 'PRO', full: 'Professional', icon: Briefcase },
|
||||
{ id: 'creative', label: 'CRE', full: 'Creative', icon: Palette },
|
||||
{ id: 'academic', label: 'ACA', full: 'Academic', icon: GraduationCap },
|
||||
{ id: 'casual', label: 'CAS', full: 'Casual', icon: Coffee },
|
||||
]
|
||||
const TONE_ICONS = [
|
||||
{ id: 'professional', icon: Briefcase },
|
||||
{ id: 'creative', icon: Palette },
|
||||
{ id: 'academic', icon: GraduationCap },
|
||||
{ id: 'casual', icon: Coffee },
|
||||
] as const
|
||||
|
||||
interface ActionDef {
|
||||
id: string
|
||||
@@ -155,10 +155,10 @@ function CopyPreviewButton({ text }: { text: string }) {
|
||||
ta.focus()
|
||||
ta.setSelectionRange(0, ta.value.length)
|
||||
let ok = false
|
||||
try { ok = document.execCommand('copy') } catch {}
|
||||
try { ok = document.execCommand('copy') } catch { }
|
||||
document.body.removeChild(ta)
|
||||
if (!ok) {
|
||||
try { navigator.clipboard.writeText(text) } catch {}
|
||||
try { navigator.clipboard.writeText(text) } catch { }
|
||||
}
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
@@ -195,40 +195,40 @@ export function ContextualAIChat({
|
||||
const { t, language } = useLanguage()
|
||||
const webSearchAvailable = useWebSearchAvailable()
|
||||
|
||||
const [activeTab, setActiveTab] = useState<'chat' | 'actions' | 'resource'>('actions')
|
||||
const [selectedTone, setSelectedTone] = useState('professional')
|
||||
const [input, setInput] = useState('')
|
||||
const [chatScope, setChatScope] = useState<'note' | 'all' | string>('note')
|
||||
const [webSearch, setWebSearch] = useState(false)
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [activeTab, setActiveTab] = useState<'chat' | 'actions' | 'resource'>('actions')
|
||||
const [selectedTone, setSelectedTone] = useState('professional')
|
||||
const [input, setInput] = useState('')
|
||||
const [chatScope, setChatScope] = useState<'note' | 'all' | string>('note')
|
||||
const [webSearch, setWebSearch] = useState(false)
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
// Action state
|
||||
const [actionLoading, setActionLoading] = useState<string | null>(null)
|
||||
const [actionPreview, setActionPreview] = useState<{ label: string; text: string } | null>(null)
|
||||
const [actionLoading, setActionLoading] = useState<string | null>(null)
|
||||
const [actionPreview, setActionPreview] = useState<{ label: string; text: string } | null>(null)
|
||||
const [showLangPicker, setShowLangPicker] = useState(false)
|
||||
const [translateTarget, setTranslateTarget] = useState('')
|
||||
|
||||
// Generate slides / diagram state
|
||||
const [generateLoading, setGenerateLoading] = useState<'slides' | 'diagram' | null>(null)
|
||||
const [generateResult, setGenerateResult] = useState<GenerateResult | null>(null)
|
||||
const [generateResult, setGenerateResult] = useState<GenerateResult | null>(null)
|
||||
const [customLangInput, setCustomLangInput] = useState('')
|
||||
// Generation options
|
||||
const [slideTheme, setSlideTheme] = useState('architectural_mono')
|
||||
const [slideStyle, setSlideStyle] = useState('professional')
|
||||
const [diagramType, setDiagramType] = useState('logic_flow')
|
||||
const [slideTheme, setSlideTheme] = useState('architectural_mono')
|
||||
const [slideStyle, setSlideStyle] = useState('professional')
|
||||
const [diagramType, setDiagramType] = useState('logic_flow')
|
||||
const [diagramStyle, setDiagramStyle] = useState('polished')
|
||||
const [diagramEmbedLoading, setDiagramEmbedLoading] = useState(false)
|
||||
|
||||
// Resource tab state
|
||||
const [resourceUrl, setResourceUrl] = useState('')
|
||||
const [resourceText, setResourceText] = useState('')
|
||||
const [resourceUrl, setResourceUrl] = useState('')
|
||||
const [resourceText, setResourceText] = useState('')
|
||||
const [resourceScraping, setResourceScraping] = useState(false)
|
||||
const [resourceMode, setResourceMode] = useState<'replace' | 'complete' | 'merge'>('complete')
|
||||
const [resourceMode, setResourceMode] = useState<'replace' | 'complete' | 'merge'>('complete')
|
||||
const [resourcePreview, setResourcePreview] = useState<{ text: string; source: string } | null>(null)
|
||||
const [resourceEnriching, setResourceEnriching] = useState(false)
|
||||
const [resourcePreviewFormat, setResourcePreviewFormat] = useState<'rendered' | 'markdown'>('rendered')
|
||||
// hoveredMsgId: which chat message shows inject actions
|
||||
const [hoveredMsgId, setHoveredMsgId] = useState<string | null>(null)
|
||||
const [hoveredMsgId, setHoveredMsgId] = useState<string | null>(null)
|
||||
|
||||
// Label regeneration state
|
||||
const [regenerateLabelsLoading, setRegenerateLabelsLoading] = useState(false)
|
||||
@@ -239,8 +239,8 @@ export function ContextualAIChat({
|
||||
const transport = useRef(new DefaultChatTransport({ api: '/api/chat' })).current
|
||||
|
||||
const buildChatBody = () => {
|
||||
const body: Record<string, any> = {
|
||||
language,
|
||||
const body: Record<string, any> = {
|
||||
language,
|
||||
webSearch,
|
||||
format: diagramInsertFormat || 'markdown'
|
||||
}
|
||||
@@ -509,7 +509,7 @@ export function ContextualAIChat({
|
||||
const handleInjectFromChat = async (msgText: string, mode: 'replace' | 'complete' | 'merge') => {
|
||||
setResourceText(msgText)
|
||||
setResourceMode(mode)
|
||||
|
||||
|
||||
if (mode === 'replace') {
|
||||
setResourcePreview({ text: msgText, source: 'chat' })
|
||||
return
|
||||
@@ -555,40 +555,37 @@ export function ContextualAIChat({
|
||||
/>
|
||||
)}
|
||||
<aside className={cn(
|
||||
'border-l border-border bg-memento-paper dark:bg-background flex flex-col flex-shrink-0 z-10 transition-all duration-300 shadow-2xl',
|
||||
'border-l border-border bg-[#FDFCFB] dark:bg-[#0D0D0D] flex flex-col z-10 transition-all duration-300 shadow-2xl',
|
||||
expanded
|
||||
? 'fixed right-0 top-0 h-screen w-[640px] z-[200]'
|
||||
: 'h-full w-[360px]',
|
||||
: 'self-stretch h-full w-[400px]',
|
||||
!expanded && className,
|
||||
)}>
|
||||
|
||||
<div className="p-6 border-b border-border shrink-0">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="min-w-0 space-y-2">
|
||||
<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" />
|
||||
IA Assistant
|
||||
</h2>
|
||||
<p className="text-[11px] text-foreground/60 uppercase tracking-wider font-medium opacity-60 truncate">
|
||||
<div className="p-6 border-b border-border/60 space-y-1.5 bg-white/50 dark:bg-black/20 backdrop-blur-md shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="flex items-center gap-2 font-serif text-xl font-medium text-ink">
|
||||
<Sparkles size={18} className="text-brand-accent" />
|
||||
{t('ai.assistantTitle') || 'IA Assistant'}
|
||||
</h3>
|
||||
<p className="text-[11px] text-concrete uppercase tracking-wider font-medium opacity-60 truncate">
|
||||
"{noteTitle || t('ai.currentNote')}"
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0 -mt-1">
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<button
|
||||
onClick={() => setExpanded(e => !e)}
|
||||
className="p-1.5 hover:bg-muted/60 rounded-full transition-colors text-foreground/40"
|
||||
className="p-1.5 hover:bg-slate-100 dark:hover:bg-white/10 rounded-lg transition-colors text-concrete"
|
||||
title={expanded ? t('ai.shrinkPanel') : t('ai.expandPanel')}
|
||||
>
|
||||
{expanded ? <Minimize2 size={18} /> : <Maximize2 size={18} />}
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1.5 hover:bg-muted/60 rounded-full transition-colors text-foreground/40 group"
|
||||
className="p-1.5 hover:bg-slate-100 dark:hover:bg-white/10 rounded-lg transition-colors text-concrete"
|
||||
>
|
||||
<div className="relative w-5 h-5 flex items-center justify-center">
|
||||
<ChevronRight size={20} className="transition-all duration-200 group-hover:opacity-0 group-hover:scale-0" />
|
||||
<X size={18} className="absolute inset-0 m-auto opacity-0 scale-0 transition-all duration-200 group-hover:opacity-100 group-hover:scale-100" />
|
||||
</div>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -604,13 +601,13 @@ export function ContextualAIChat({
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id as any)}
|
||||
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",
|
||||
activeTab === tab.id ? 'text-foreground' : 'text-foreground/60 hover:text-foreground'
|
||||
"flex-1 py-3 text-[10px] font-bold uppercase tracking-[0.2em] transition-all relative",
|
||||
activeTab === tab.id ? 'text-brand-accent' : 'text-concrete hover:text-ink/60'
|
||||
)}
|
||||
>
|
||||
{tab.label}
|
||||
{activeTab === tab.id && (
|
||||
<motion.div layoutId="activeTab" className="absolute bottom-0 left-0 right-0 h-[2px] bg-memento-blue" />
|
||||
<motion.div layoutId="activeTab" className="absolute bottom-0 left-0 right-0 h-0.5 bg-brand-accent" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
@@ -618,80 +615,150 @@ export function ContextualAIChat({
|
||||
|
||||
<div className="flex-1 flex flex-col min-h-0 relative">
|
||||
{actionPreview && (
|
||||
<div className="absolute inset-0 z-20 flex flex-col bg-memento-paper/95 dark:bg-background/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-[#FDFCFB]/95 dark:bg-[#0D0D0D]/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">
|
||||
<p className="text-[10px] font-bold uppercase tracking-widest text-memento-blue">{actionPreview.label}</p>
|
||||
<button onClick={handleDiscardPreview} className="text-foreground/40 hover:text-foreground"><X size={18} /></button>
|
||||
<p className="text-[10px] font-bold uppercase tracking-widest text-brand-accent">{actionPreview.label}</p>
|
||||
<button onClick={handleDiscardPreview} className="text-concrete hover:text-ink"><X size={18} /></button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-6 custom-scrollbar">
|
||||
<div className="bg-card/60 backdrop-blur-sm border border-border p-6 rounded-2xl shadow-sm leading-relaxed text-sm">
|
||||
<div className="bg-white/60 dark:bg-white/5 border border-border p-6 rounded-2xl leading-relaxed text-sm">
|
||||
<MarkdownContent content={actionPreview.text} />
|
||||
</div>
|
||||
</div>
|
||||
<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-foreground/40 hover:text-foreground transition-all">{t('ai.cancel')}</button>
|
||||
<button onClick={handleDiscardPreview} className="flex-1 py-3.5 text-[10px] font-bold uppercase tracking-widest text-concrete hover:text-ink transition-all">{t('ai.cancel')}</button>
|
||||
<CopyPreviewButton text={actionPreview.text} />
|
||||
<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">{t('ai.applyToNote')}</button>
|
||||
<button onClick={handleApplyPreview} className="flex-1 py-3.5 bg-ink text-paper rounded-xl text-[10px] font-bold uppercase tracking-widest shadow-lg transition-all hover:opacity-90">{t('ai.applyToNote')}</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{resourcePreview && (
|
||||
<div className="absolute inset-0 z-20 flex flex-col bg-memento-paper/95 dark: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">
|
||||
<p className="text-[10px] font-bold uppercase tracking-widest text-memento-blue">
|
||||
<div className="absolute inset-0 z-20 flex flex-col bg-[#FDFCFB]/95 dark:bg-[#0D0D0D]/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">
|
||||
<p className="text-[10px] font-bold uppercase tracking-widest text-brand-accent">
|
||||
{resourcePreview.source === 'chat' ? t('ai.resourcePreviewInjectFromChat') : t('ai.resourcePreviewAiTitle')}
|
||||
</p>
|
||||
<button onClick={() => setResourcePreview(null)} className="text-foreground/40 hover:text-foreground">
|
||||
<button onClick={() => setResourcePreview(null)} className="text-concrete hover:text-ink">
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-6 custom-scrollbar">
|
||||
<div className="bg-card/60 backdrop-blur-sm border border-border p-6 rounded-2xl shadow-sm leading-relaxed text-sm">
|
||||
<div className="bg-white/60 dark:bg-white/5 border border-border p-6 rounded-2xl leading-relaxed text-sm">
|
||||
<MarkdownContent content={resourcePreview.text} />
|
||||
</div>
|
||||
</div>
|
||||
<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-foreground/40 hover:text-foreground transition-all">{t('ai.cancel')}</button>
|
||||
<button onClick={() => setResourcePreview(null)} className="flex-1 py-3.5 text-[10px] font-bold uppercase tracking-widest text-concrete hover:text-ink transition-all">{t('ai.cancel')}</button>
|
||||
<CopyPreviewButton text={resourcePreview.text} />
|
||||
<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">{t('ai.applyToNote')}</button>
|
||||
<button onClick={handleApplyResourcePreview} className="flex-1 py-3.5 bg-brand-accent text-white rounded-xl text-[10px] font-bold uppercase tracking-widest shadow-lg shadow-brand-accent/20 transition-all hover:opacity-90">{t('ai.applyToNote')}</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
{activeTab === 'chat' && (
|
||||
<motion.div
|
||||
<motion.div
|
||||
key="chat"
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
className="flex flex-col flex-1 min-h-0"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
className="flex flex-col flex-1 overflow-hidden"
|
||||
>
|
||||
<div className="flex-1 overflow-y-auto p-6 custom-scrollbar space-y-8">
|
||||
{messages.length === 0 && (
|
||||
<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-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" />
|
||||
<div className="flex-1 overflow-y-auto p-6 custom-scrollbar space-y-6 min-h-0">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-[10px] uppercase tracking-[0.2em] font-bold text-concrete">{t('ai.writingTone')}</label>
|
||||
<div className="flex items-center gap-1">
|
||||
{TONE_ICONS.map((tone) => {
|
||||
const Icon = tone.icon
|
||||
return (
|
||||
<button
|
||||
key={tone.id}
|
||||
onClick={() => setSelectedTone(tone.id)}
|
||||
title={t(`ai.tones.${tone.id}`)}
|
||||
className={cn(
|
||||
'w-8 h-8 rounded-lg flex items-center justify-center text-[9px] font-bold transition-all border',
|
||||
selectedTone === tone.id
|
||||
? 'bg-brand-accent text-white border-brand-accent shadow-sm'
|
||||
: 'bg-white/50 dark:bg-white/5 border-border/40 text-concrete hover:border-brand-accent/40'
|
||||
)}
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<p className="text-xs font-serif italic text-foreground/40 leading-relaxed max-w-[200px]">{t('ai.askToStart')}</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setChatScope('note')}
|
||||
className={cn(
|
||||
'w-full p-2.5 border rounded-xl text-[11px] flex items-center justify-between transition-all',
|
||||
chatScope === 'note' ? 'bg-brand-accent/5 border-brand-accent/30' : 'bg-white/50 dark:bg-white/5 border-border/40 hover:border-ink/20'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<BookOpen size={14} className="text-brand-accent/60" />
|
||||
<span className={cn('font-medium', chatScope === 'note' ? 'text-brand-accent' : 'text-concrete')}>{t('ai.thisNote')}</span>
|
||||
</div>
|
||||
{chatScope === 'note' && (
|
||||
<span className="text-[8px] bg-brand-accent/10 text-brand-accent px-1.5 py-0.5 rounded-full uppercase font-bold">{t('ai.scopeAutoBadge')}</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-2 px-2">
|
||||
<div className="h-px flex-1 bg-border/40" />
|
||||
<span className="text-[9px] font-bold text-concrete uppercase tracking-widest">+ Carnet</span>
|
||||
<div className="h-px flex-1 bg-border/40" />
|
||||
</div>
|
||||
|
||||
<HierarchicalNotebookSelector
|
||||
notebooks={(notebooks || []).filter(nb => !nb.trashedAt)}
|
||||
selectedId={chatScope !== 'note' && chatScope !== 'all' ? chatScope : null}
|
||||
onSelect={(id) => setChatScope(id)}
|
||||
placeholder={t('ai.chatNotebookSelectPlaceholder')}
|
||||
className="w-full"
|
||||
size="sm"
|
||||
dropUp
|
||||
/>
|
||||
</div>
|
||||
|
||||
{messages.length === 0 && (
|
||||
<div className="h-48 flex flex-col items-center justify-center text-center space-y-3 text-concrete/30">
|
||||
<div className="w-12 h-12 rounded-full border border-dashed border-concrete/10 flex items-center justify-center">
|
||||
<MessageSquare size={18} />
|
||||
</div>
|
||||
<p className="text-[11px] italic leading-relaxed px-12">{t('ai.welcomeMsg')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{messages.map((msg: UIMessage) => {
|
||||
const content = getMessageContent(msg)
|
||||
if (msg.role === 'assistant' && !content) return null
|
||||
const isAssistant = msg.role === 'assistant'
|
||||
return (
|
||||
<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={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>}
|
||||
<div key={msg.id} className={cn('flex gap-3', !isAssistant && 'flex-row-reverse')} onMouseEnter={() => isAssistant && setHoveredMsgId(msg.id)} onMouseLeave={() => setHoveredMsgId(null)}>
|
||||
<div className={cn(
|
||||
'w-8 h-8 rounded-xl flex items-center justify-center flex-shrink-0',
|
||||
!isAssistant ? 'bg-ink text-paper' : 'bg-brand-accent/10 text-brand-accent',
|
||||
)}>
|
||||
{!isAssistant ? 'U' : <Bot className="h-4 w-4" />}
|
||||
</div>
|
||||
<div className="relative group/msg max-w-[85%]">
|
||||
<div className={cn(
|
||||
'p-3.5 rounded-2xl text-sm leading-relaxed',
|
||||
!isAssistant
|
||||
? 'bg-ink text-paper rounded-tr-sm'
|
||||
: 'bg-white/60 dark:bg-white/5 border border-border rounded-tl-sm text-ink',
|
||||
)}>
|
||||
{isAssistant ? <MarkdownContent content={content} /> : <p>{content}</p>}
|
||||
</div>
|
||||
{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">
|
||||
<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">{t('ai.injectReplace')}</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">{t('ai.injectComplete')}</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">{t('ai.injectMerge')}</button>
|
||||
<div className="flex gap-2 mt-2 opacity-0 group-hover/msg:opacity-100 transition-all">
|
||||
<button onClick={() => handleInjectFromChat(content, 'replace')} className="px-2.5 py-1 rounded-lg text-[8px] font-bold uppercase tracking-widest bg-ink text-paper hover:opacity-90">{t('ai.injectReplace')}</button>
|
||||
<button onClick={() => handleInjectFromChat(content, 'complete')} className="px-2.5 py-1 rounded-lg text-[8px] font-bold uppercase tracking-widest bg-white/50 dark:bg-white/5 border border-border text-ink hover:bg-white dark:hover:bg-white/10">{t('ai.injectComplete')}</button>
|
||||
<button onClick={() => handleInjectFromChat(content, 'merge')} className="px-2.5 py-1 rounded-lg text-[8px] font-bold uppercase tracking-widest bg-white/50 dark:bg-white/5 border border-border text-ink hover:bg-white dark:hover:bg-white/10">{t('ai.injectMerge')}</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -699,104 +766,22 @@ export function ContextualAIChat({
|
||||
)
|
||||
})}
|
||||
{isLoading && (
|
||||
<div className="flex flex-col gap-3">
|
||||
<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-3">
|
||||
<div className="w-8 h-8 rounded-xl bg-brand-accent/10 text-brand-accent flex items-center justify-center flex-shrink-0">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
<div className="bg-white/60 dark:bg-white/5 border border-border p-3.5 rounded-2xl rounded-tl-sm">
|
||||
<Loader2 className="h-4 w-4 animate-spin text-concrete" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* ── Chat Footer: Context + Tone + Input ── */}
|
||||
<div className="px-6 py-8 border-t border-border shrink-0 space-y-6">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="space-y-3">
|
||||
<label className="text-[10px] uppercase tracking-[0.25em] font-bold text-foreground/40 px-1">{t('ai.chatPanelContext')}</label>
|
||||
<div className="flex flex-col gap-2">
|
||||
<button
|
||||
onClick={() => setChatScope('note')}
|
||||
className={cn(
|
||||
'w-full p-3 border rounded-lg text-xs flex items-center gap-2 transition-all',
|
||||
chatScope === 'note' ? 'bg-blueprint/10 border-blueprint/30 text-blueprint font-bold' : 'bg-card/60 border-border hover:border-foreground/20 text-foreground/60'
|
||||
)}
|
||||
>
|
||||
<BookOpen size={14} className="text-blueprint/60" />
|
||||
<span>{t('ai.thisNote')}</span>
|
||||
<span className="ml-auto text-[8px] bg-blueprint/10 text-blueprint px-1.5 py-0.5 rounded uppercase font-bold">{t('ai.scopeAutoBadge')}</span>
|
||||
</button>
|
||||
<div className="flex items-center gap-2 px-2">
|
||||
<div className="h-px flex-1 bg-border/40" />
|
||||
<span className="text-[9px] font-bold text-muted-foreground uppercase tracking-widest">{t('ai.chatPanelNotebookPlus')}</span>
|
||||
<div className="h-px flex-1 bg-border/40" />
|
||||
</div>
|
||||
<HierarchicalNotebookSelector
|
||||
notebooks={(notebooks || []).filter(nb => !nb.trashedAt)}
|
||||
selectedId={chatScope !== 'note' && chatScope !== 'all' ? chatScope : null}
|
||||
onSelect={(id) => setChatScope(id)}
|
||||
placeholder={t('ai.chatNotebookSelectPlaceholder')}
|
||||
className="w-full"
|
||||
size="sm"
|
||||
dropUp
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<label className="text-[10px] uppercase tracking-[0.25em] font-bold text-foreground/40 px-1">{t('ai.chatPanelWritingTone')}</label>
|
||||
<div className="grid grid-cols-4 gap-1.5">
|
||||
{TONES.map((tone) => {
|
||||
const Icon = tone.icon
|
||||
const isActive = selectedTone === tone.id
|
||||
return (
|
||||
<button
|
||||
key={tone.id}
|
||||
onClick={() => setSelectedTone(tone.id)}
|
||||
className={cn(
|
||||
'h-[52px] rounded-xl border transition-all flex flex-col items-center justify-center gap-1.5 shadow-sm',
|
||||
isActive
|
||||
? 'bg-memento-blue/10 border-memento-blue text-memento-blue'
|
||||
: 'bg-card/60 border-border text-foreground/40 hover:border-foreground/20'
|
||||
)}
|
||||
>
|
||||
<Icon size={14} className={isActive ? 'text-memento-blue' : 'text-foreground/40'} />
|
||||
<span className="text-[9px] font-bold uppercase tracking-tight">{tone.label}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<textarea
|
||||
rows={4}
|
||||
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={t('ai.chatNoteQuestionPlaceholder')}
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend() } }}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<div className="absolute right-4 bottom-4 flex gap-2">
|
||||
<button
|
||||
onClick={() => setWebSearch(!webSearch)}
|
||||
className={cn("p-2.5 rounded-xl transition-colors", webSearch ? "text-memento-blue bg-memento-blue/10" : "text-foreground/20 hover:text-foreground")}
|
||||
title={t('ai.webSearchLabel')}
|
||||
>
|
||||
<Globe size={18} />
|
||||
</button>
|
||||
<button onClick={handleSend} disabled={!input.trim() || isLoading} className="p-2.5 bg-memento-blue text-white rounded-xl transition-all hover:scale-105 active:scale-95 shadow-lg shadow-memento-blue/20 disabled:opacity-30">
|
||||
<Send size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[9px] text-foreground/30 text-center mt-2 uppercase tracking-[0.2em] font-bold italic">{t('ai.newLineHint')}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{activeTab === 'actions' && (
|
||||
<motion.div
|
||||
<motion.div
|
||||
key="actions"
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
@@ -804,31 +789,31 @@ export function ContextualAIChat({
|
||||
className="flex flex-col flex-1 overflow-y-auto p-6 space-y-10 custom-scrollbar"
|
||||
>
|
||||
{notebookId && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-px flex-1 bg-border/40" />
|
||||
<h4 className="text-[9px] uppercase tracking-[0.3em] font-bold text-foreground/40 whitespace-nowrap">{t('ai.organization') || 'Organisation'}</h4>
|
||||
<div className="h-px flex-1 bg-border/40" />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRegenerateLabels}
|
||||
className="w-full flex items-center gap-3 p-4 bg-card border border-border rounded-xl transition-all hover:border-memento-blue/30 cursor-pointer"
|
||||
>
|
||||
<div className="p-2 bg-card rounded-lg text-memento-blue shrink-0"><TagIcon size={18} /></div>
|
||||
<div className="flex-1 text-left">
|
||||
<h5 className="text-[10px] font-bold text-foreground">{t('ai.autoLabels.regenerate') || 'Labels IA'}</h5>
|
||||
<p className="text-[8px] text-foreground/40 uppercase tracking-tight">{notebookName || ''}</p>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-px flex-1 bg-border/40" />
|
||||
<h4 className="text-[10px] uppercase tracking-[0.25em] font-bold text-concrete whitespace-nowrap">{t('ai.organization') || 'Organisation'}</h4>
|
||||
<div className="h-px flex-1 bg-border/40" />
|
||||
</div>
|
||||
<RefreshCw size={14} className="text-memento-blue shrink-0" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRegenerateLabels}
|
||||
className="w-full flex items-center gap-3 p-4 bg-white/50 dark:bg-white/5 border border-border rounded-xl transition-all hover:border-brand-accent/30 cursor-pointer"
|
||||
>
|
||||
<div className="p-2 bg-slate-50 dark:bg-white/10 rounded-lg text-brand-accent shrink-0"><TagIcon size={18} /></div>
|
||||
<div className="flex-1 text-left">
|
||||
<h5 className="text-[10px] font-bold text-ink">{t('ai.autoLabels.regenerate') || 'Labels IA'}</h5>
|
||||
<p className="text-[8px] text-concrete uppercase tracking-tight">{notebookName || ''}</p>
|
||||
</div>
|
||||
<RefreshCw size={14} className="text-brand-accent shrink-0" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-px flex-1 bg-border/40" />
|
||||
<h4 className="text-[9px] uppercase tracking-[0.3em] font-bold text-foreground/40 whitespace-nowrap">{t('ai.transformations')}</h4>
|
||||
<h4 className="text-[10px] uppercase tracking-[0.25em] font-bold text-concrete whitespace-nowrap">{t('ai.transformations')}</h4>
|
||||
<div className="h-px flex-1 bg-border/40" />
|
||||
</div>
|
||||
|
||||
@@ -837,7 +822,7 @@ export function ContextualAIChat({
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
onClick={onUndoLastAction}
|
||||
className="w-full py-3.5 bg-memento-blue/20 border border-memento-blue/50 rounded-xl flex items-center justify-center gap-2 text-[11px] font-bold text-memento-blue uppercase tracking-[0.2em] hover:bg-memento-blue/30 transition-all shadow-md"
|
||||
className="w-full py-3.5 bg-brand-accent/20 border border-brand-accent/50 rounded-xl flex items-center justify-center gap-2 text-[11px] font-bold text-brand-accent uppercase tracking-[0.2em] hover:bg-brand-accent/30 transition-all shadow-md"
|
||||
>
|
||||
<RotateCcw size={12} /> {t('ai.undoLastAction')}
|
||||
</motion.button>
|
||||
@@ -848,25 +833,25 @@ export function ContextualAIChat({
|
||||
const isActive = action.id === 'translate' && showLangPicker
|
||||
const Icon = action.icon
|
||||
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-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-card/60 transition-colors group-hover:bg-foreground group-hover:text-background shadow-sm", loading && "animate-pulse", isActive && "bg-memento-blue text-white")}>
|
||||
<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/50 dark:bg-white/5 border border-border rounded-xl transition-all group hover:border-ink/20 shadow-sm", isActive ? "border-brand-accent bg-brand-accent/5" : "")}>
|
||||
<div className={cn("p-2 rounded-lg bg-slate-50 dark:bg-white/10 transition-colors group-hover:bg-brand-accent group-hover:text-white shadow-sm text-concrete", loading && "animate-pulse", isActive && "bg-brand-accent text-white")}>
|
||||
{loading ? <Loader2 size={14} className="animate-spin" /> : <Icon size={14} />}
|
||||
</div>
|
||||
<span className={cn("text-[10px] font-bold uppercase tracking-widest", isActive ? "text-memento-blue" : "text-foreground/80")}>{t(action.i18nKey)}</span>
|
||||
<span className={cn("text-[10px] font-bold text-ink/80 uppercase tracking-widest", isActive ? "text-brand-accent" : "")}>{t(action.i18nKey)}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
{showLangPicker && (
|
||||
<motion.div
|
||||
<motion.div
|
||||
key="lang-picker"
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="col-span-2 overflow-hidden"
|
||||
>
|
||||
<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="mt-2 p-5 bg-white/50 dark:bg-white/5 border border-brand-accent/30 rounded-2xl space-y-5">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{TRANSLATE_LANGUAGE_OPTIONS.map(({ api, labelKey }) => (
|
||||
<button
|
||||
@@ -875,18 +860,18 @@ export function ContextualAIChat({
|
||||
className={cn(
|
||||
"py-2 px-1 rounded-lg border text-[10px] font-bold uppercase tracking-tighter transition-all",
|
||||
translateTarget === api
|
||||
? "bg-memento-blue border-memento-blue text-white shadow-md shadow-memento-blue/20"
|
||||
: "bg-card/60 border-border text-foreground/60 hover:border-foreground/20"
|
||||
? "bg-brand-accent border-brand-accent text-white shadow-md shadow-brand-accent/20"
|
||||
: "bg-white/50 dark:bg-white/5 border-border text-concrete hover:border-ink/20"
|
||||
)}
|
||||
>
|
||||
{t(labelKey)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
<div className="space-y-2">
|
||||
<span className="text-[8px] uppercase tracking-[0.2em] font-bold text-foreground/40 px-1">{t('ai.otherLanguage')}</span>
|
||||
<input
|
||||
<span className="text-[8px] uppercase tracking-[0.2em] font-bold text-concrete px-1">{t('ai.otherLanguage')}</span>
|
||||
<input
|
||||
type="text"
|
||||
value={customLangInput}
|
||||
onChange={(e) => {
|
||||
@@ -894,14 +879,14 @@ export function ContextualAIChat({
|
||||
setTranslateTarget(e.target.value)
|
||||
}}
|
||||
placeholder={t('languages.customPlaceholder')}
|
||||
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"
|
||||
className="w-full bg-white/50 dark:bg-white/5 border border-border rounded-xl px-4 py-2.5 text-[11px] outline-none focus:border-brand-accent transition-all text-ink"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
<button
|
||||
onClick={() => handleAction(ACTION_IDS.find(a => a.id === 'translate')!, translateTarget)}
|
||||
disabled={!translateTarget || !!actionLoading}
|
||||
className="w-full py-3 bg-memento-blue text-white rounded-xl text-[10px] font-bold uppercase tracking-[0.2em] flex items-center justify-center gap-2 hover:opacity-90 disabled:opacity-50 shadow-lg shadow-memento-blue/20"
|
||||
className="w-full py-3 bg-brand-accent text-white rounded-xl text-[10px] font-bold uppercase tracking-[0.2em] flex items-center justify-center gap-2 hover:opacity-90 disabled:opacity-50 shadow-lg shadow-brand-accent/20"
|
||||
>
|
||||
<Languages size={14} /> {t('ai.translateNow')}
|
||||
</button>
|
||||
@@ -911,21 +896,21 @@ export function ContextualAIChat({
|
||||
</AnimatePresence>
|
||||
|
||||
{diagramInsertFormat === 'html' ? (
|
||||
<button
|
||||
onClick={() => handleAction(ACTION_IDS.find(a => a.id === 'markdown')!)}
|
||||
<button
|
||||
onClick={() => handleAction(ACTION_IDS.find(a => a.id === 'markdown')!)}
|
||||
disabled={!!actionLoading}
|
||||
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"
|
||||
className="col-span-2 flex items-center justify-center gap-3 py-3.5 bg-white/50 dark:bg-white/5 border border-border rounded-xl text-[10px] font-bold text-ink/80 hover:bg-white dark:hover:bg-white/10 transition-all hover:border-ink/20 uppercase tracking-[0.2em] shadow-sm disabled:opacity-50"
|
||||
>
|
||||
<Code size={14} className="text-foreground/40" />
|
||||
<Code size={14} className="text-concrete" />
|
||||
{actionLoading === 'markdown' ? <Loader2 size={14} className="animate-spin" /> : t('ai.action.toMarkdown')}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => handleAction(ACTION_IDS.find(a => a.id === 'toRichText')!)}
|
||||
<button
|
||||
onClick={() => handleAction(ACTION_IDS.find(a => a.id === 'toRichText')!)}
|
||||
disabled={!!actionLoading}
|
||||
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"
|
||||
className="col-span-2 flex items-center justify-center gap-3 py-3.5 bg-white/50 dark:bg-white/5 border border-border rounded-xl text-[10px] font-bold text-ink/80 hover:bg-white dark:hover:bg-white/10 transition-all hover:border-ink/20 uppercase tracking-[0.2em] shadow-sm disabled:opacity-50"
|
||||
>
|
||||
<Wand2 size={14} className="text-foreground/40" />
|
||||
<Wand2 size={14} className="text-concrete" />
|
||||
{actionLoading === 'toRichText' ? <Loader2 size={14} className="animate-spin" /> : t('ai.action.toRichText')}
|
||||
</button>
|
||||
)}
|
||||
@@ -935,17 +920,17 @@ export function ContextualAIChat({
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-px flex-1 bg-border/40" />
|
||||
<h4 className="text-[9px] uppercase tracking-[0.3em] font-bold text-foreground/40 whitespace-nowrap">{t('ai.generationTools')}</h4>
|
||||
<h4 className="text-[10px] uppercase tracking-[0.25em] font-bold text-concrete whitespace-nowrap">{t('ai.generationTools')}</h4>
|
||||
<div className="h-px flex-1 bg-border/40" />
|
||||
</div>
|
||||
|
||||
<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="group relative p-6 rounded-2xl bg-white border border-border hover:border-brand-accent/30 transition-all duration-500 overflow-hidden">
|
||||
<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-brand-accent" />
|
||||
</div>
|
||||
<div className="relative space-y-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-card/60 rounded-lg text-memento-blue"><Layout size={18} /></div>
|
||||
<div className="p-2 bg-slate-50 rounded-lg text-brand-accent"><Layout size={18} /></div>
|
||||
<div className="space-y-0.5">
|
||||
<h5 className="text-sm font-bold text-foreground leading-none">{t('ai.generate.slides')}</h5>
|
||||
<p className="text-[9px] text-foreground/40 uppercase tracking-tight">{t('ai.generate.sectionLabel')}</p>
|
||||
@@ -954,7 +939,7 @@ export function ContextualAIChat({
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<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-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">
|
||||
<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-brand-accent/10 transition-all cursor-pointer text-foreground">
|
||||
<option value="architectural_mono">{t('ai.generate.themeArchitecturalMono')}</option>
|
||||
<option value="vibrant_tech">{t('ai.generate.themeVibrantTech')}</option>
|
||||
<option value="minimal_silk">{t('ai.generate.themeMinimalSilk')}</option>
|
||||
@@ -962,25 +947,25 @@ export function ContextualAIChat({
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<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-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">
|
||||
<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-brand-accent/10 transition-all cursor-pointer text-foreground">
|
||||
<option value="professional">{t('ai.generate.styleProfessional')}</option>
|
||||
<option value="creative">{t('ai.generate.styleCreative')}</option>
|
||||
<option value="brutalist">{t('ai.generate.styleBrutalist')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={() => handleGenerate('slides')} disabled={!!generateLoading} className="w-full py-3.5 bg-memento-blue text-white rounded-xl text-[10px] font-bold flex items-center justify-center gap-2 hover:opacity-90 transition-all shadow-lg shadow-memento-blue/20 uppercase tracking-[0.2em] disabled:opacity-50">
|
||||
<button onClick={() => handleGenerate('slides')} disabled={!!generateLoading} className="w-full py-3.5 bg-brand-accent text-white rounded-xl text-[10px] font-bold flex items-center justify-center gap-2 hover:opacity-90 transition-all shadow-lg shadow-brand-accent/20 uppercase tracking-[0.2em] disabled:opacity-50">
|
||||
{generateLoading === 'slides' ? <Loader2 size={14} className="animate-spin" /> : <><Presentation size={14} className="opacity-80" /> {t('ai.generating')}</>}
|
||||
</button>
|
||||
|
||||
{generateResult?.type === 'slides' && generateResult.canvasId && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mt-4 p-4 bg-memento-blue/10 border border-memento-blue/20 rounded-xl space-y-3"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[9px] font-bold text-memento-blue uppercase tracking-widest flex items-center gap-1.5">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mt-4 p-4 bg-brand-accent/10 border border-brand-accent/20 rounded-xl space-y-3"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[9px] font-bold text-brand-accent uppercase tracking-widest flex items-center gap-1.5">
|
||||
<Check size={12} /> {t('ai.presentationReadyBadge')}
|
||||
</span>
|
||||
<a
|
||||
@@ -1017,7 +1002,7 @@ export function ContextualAIChat({
|
||||
mToast.error(t('ai.downloadFailedToast'))
|
||||
}
|
||||
}}
|
||||
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"
|
||||
className="flex items-center justify-center gap-2 w-full py-2.5 bg-brand-accent text-white rounded-lg text-[10px] font-bold uppercase tracking-[0.15em] hover:opacity-90 transition-opacity shadow-sm"
|
||||
>
|
||||
<Download size={13} />
|
||||
{t('ai.pptxDownloadButton')}
|
||||
@@ -1028,13 +1013,13 @@ export function ContextualAIChat({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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="group relative p-6 rounded-2xl bg-white border border-border hover:border-brand-accent/30 transition-all duration-500 overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
|
||||
<BookOpen size={80} className="text-primary" />
|
||||
<BookOpen size={80} className="text-brand-accent" />
|
||||
</div>
|
||||
<div className="relative space-y-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-card/60 rounded-lg text-primary"><BookOpen size={18} /></div>
|
||||
<div className="p-2 bg-slate-50 rounded-lg text-brand-accent"><BookOpen size={18} /></div>
|
||||
<div className="space-y-0.5">
|
||||
<h5 className="text-sm font-bold text-foreground leading-none">{t('ai.generate.diagram')}</h5>
|
||||
<p className="text-[9px] text-foreground/40 uppercase tracking-tight">{t('ai.generate.diagramReadyHint')}</p>
|
||||
@@ -1043,7 +1028,7 @@ export function ContextualAIChat({
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<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-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">
|
||||
<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-brand-accent/10 transition-all cursor-pointer text-foreground">
|
||||
<option value="auto">{t('ai.generate.typeAuto')}</option>
|
||||
<option value="flowchart">{t('ai.generate.typeFlowchart')}</option>
|
||||
<option value="mind_map">{t('ai.generate.typeMindMap')}</option>
|
||||
@@ -1055,7 +1040,7 @@ export function ContextualAIChat({
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<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-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">
|
||||
<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-brand-accent/10 transition-all cursor-pointer text-foreground">
|
||||
<option value="sketchy">{t('ai.generate.styleSketchy')}</option>
|
||||
<option value="soft">{t('ai.generate.styleSoft')}</option>
|
||||
<option value="minimal">{t('ai.generate.styleMinimal')}</option>
|
||||
@@ -1066,20 +1051,20 @@ export function ContextualAIChat({
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<button onClick={() => handleGenerate('diagram')} disabled={!!generateLoading} className="w-full py-3.5 bg-brand-accent text-white rounded-xl text-[10px] font-bold flex items-center justify-center gap-2 hover:opacity-90 transition-all shadow-lg shadow-brand-accent/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" /></>}
|
||||
</button>
|
||||
|
||||
{generateResult?.type === 'diagram' && generateResult.canvasId && (
|
||||
<motion.div
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mt-4 p-4 bg-primary/10 border border-primary/20 rounded-xl space-y-3"
|
||||
className="mt-4 p-4 bg-brand-accent/10 border border-brand-accent/20 rounded-xl space-y-3"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[9px] font-bold text-primary uppercase tracking-widest">{t('ai.generate.diagramReady')}</span>
|
||||
<span className="text-[9px] font-bold text-brand-accent uppercase tracking-widest">{t('ai.generate.diagramReady')}</span>
|
||||
<div className="flex gap-2">
|
||||
<a
|
||||
<a
|
||||
href={`/lab?id=${generateResult.canvasId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -1088,10 +1073,10 @@ export function ContextualAIChat({
|
||||
>
|
||||
<ExternalLink size={14} />
|
||||
</a>
|
||||
<button
|
||||
<button
|
||||
onClick={() => handleEmbedDiagramInNote(generateResult.canvasId!)}
|
||||
disabled={diagramEmbedLoading}
|
||||
className="p-2 bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity disabled:opacity-50"
|
||||
className="p-2 bg-brand-accent text-white rounded-lg hover:opacity-90 transition-opacity disabled:opacity-50"
|
||||
title={t('ai.generate.insertDiagramInNote')}
|
||||
>
|
||||
{diagramEmbedLoading ? <Loader2 size={14} className="animate-spin" /> : <ImagePlus size={14} />}
|
||||
@@ -1105,14 +1090,14 @@ export function ContextualAIChat({
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-4 py-8 opacity-20">
|
||||
<PenTool size={20} />
|
||||
<span className="text-[9px] font-bold uppercase tracking-[0.3em] whitespace-nowrap italic text-center">{t('nav.workspace')}</span>
|
||||
<PenTool size={20} />
|
||||
<span className="text-[9px] font-bold uppercase tracking-[0.3em] whitespace-nowrap italic text-center">{t('nav.workspace')}</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{activeTab === 'resource' && (
|
||||
<motion.div
|
||||
<motion.div
|
||||
key="resource"
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
@@ -1121,32 +1106,32 @@ export function ContextualAIChat({
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
<label className="text-[9px] uppercase tracking-[0.3em] font-bold text-foreground/40">{t('ai.resource.urlLabel')}</label>
|
||||
<label className="text-[10px] uppercase tracking-[0.2em] font-bold text-concrete">{t('ai.resource.urlLabel')}</label>
|
||||
<div className="relative">
|
||||
<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-foreground/20" />
|
||||
<input type="text" placeholder="https://..." className="w-full bg-white/50 dark:bg-white/5 border border-border rounded-xl pl-4 pr-12 py-3 text-xs outline-none focus:border-brand-accent transition-colors text-ink" value={resourceUrl} onChange={e => setResourceUrl(e.target.value)} />
|
||||
<Globe size={14} className="absolute right-3 top-1/2 -translate-y-1/2 text-concrete/40" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<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-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)} />
|
||||
<label className="text-[10px] uppercase tracking-[0.2em] font-bold text-concrete">{t('ai.resource.resourceText')}</label>
|
||||
<textarea rows={10} placeholder={t('ai.resource.resourcePlaceholder')} className="w-full bg-white/50 dark:bg-white/5 border border-border rounded-xl p-4 text-xs outline-none focus:border-brand-accent transition-colors resize-none leading-relaxed text-ink" value={resourceText} onChange={e => setResourceText(e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<label className="text-[9px] uppercase tracking-[0.3em] font-bold text-foreground/40">{t('ai.resource.integrationMode')}</label>
|
||||
<label className="text-[10px] uppercase tracking-[0.2em] font-bold text-concrete">{t('ai.resource.integrationMode')}</label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{[
|
||||
{[
|
||||
{ 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: 'merge', label: t('ai.resource.modeMerge'), sub: t('ai.resource.modeMergeDesc') },
|
||||
].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-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-memento-blue' : 'text-foreground')}>{mode.label}</span>
|
||||
<span className="text-[8px] text-foreground/40 leading-tight mt-1 font-medium">{mode.sub}</span>
|
||||
<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-brand-accent/5 border-brand-accent/30' : 'bg-white border-border hover:bg-slate-50 dark:hover:bg-white/5')}>
|
||||
<span className={cn("text-[10px] font-bold", resourceMode === mode.id ? 'text-brand-accent' : 'text-ink')}>{mode.label}</span>
|
||||
<span className="text-[8px] text-concrete opacity-60 leading-tight mt-1 font-medium">{mode.sub}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={handleResourcePreview} disabled={resourceEnriching || resourceScraping || !resourceText.trim()} className="w-full py-4 bg-memento-blue text-white rounded-xl text-[11px] font-bold uppercase tracking-[0.2em] flex items-center justify-center gap-3 hover:opacity-90 transition-opacity shadow-lg shadow-memento-blue/20 disabled:opacity-50">
|
||||
<button onClick={handleResourcePreview} disabled={resourceEnriching || resourceScraping || !resourceText.trim()} className="w-full py-4 bg-brand-accent text-white rounded-xl text-[11px] font-bold uppercase tracking-[0.2em] flex items-center justify-center gap-3 hover:opacity-90 transition-opacity shadow-lg shadow-brand-accent/20 disabled:opacity-50">
|
||||
{resourceEnriching || resourceScraping ? <Loader2 size={18} className="animate-spin" /> : <Sparkles size={18} />}
|
||||
{t('ai.resource.generatePreview')}
|
||||
</button>
|
||||
@@ -1155,6 +1140,54 @@ export function ContextualAIChat({
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Textarea — only on chat tab */}
|
||||
<AnimatePresence>
|
||||
{activeTab === 'chat' && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="p-6 bg-white/40 dark:bg-black/20 border-t border-border backdrop-blur-xl shrink-0"
|
||||
>
|
||||
<div className="relative group/chat">
|
||||
<textarea
|
||||
rows={4}
|
||||
placeholder={t('ai.chatPlaceholder')}
|
||||
className="w-full bg-white/80 dark:bg-white/5 border border-border rounded-[24px] p-5 pr-14 text-sm outline-none focus:border-brand-accent focus:ring-4 ring-brand-accent/5 transition-all resize-none leading-relaxed font-light shadow-inner text-ink"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend() }
|
||||
}}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<div className="absolute right-4 bottom-4 flex flex-col gap-2">
|
||||
{isLoading ? (
|
||||
<button onClick={() => stop()} className="p-2.5 bg-rose-500 text-white rounded-xl transition-all hover:scale-110 active:scale-95 shadow-lg shadow-rose-500/20">
|
||||
<Square size={16} />
|
||||
</button>
|
||||
) : (
|
||||
<button onClick={handleSend} disabled={!input.trim()} className="p-2.5 bg-brand-accent text-white rounded-xl transition-all hover:scale-110 active:scale-95 shadow-lg shadow-brand-accent/20 disabled:opacity-40 disabled:pointer-events-none">
|
||||
<Send size={16} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="absolute left-6 bottom-4 flex gap-3 text-concrete/40">
|
||||
<button
|
||||
onClick={() => webSearchAvailable && setWebSearch(!webSearch)}
|
||||
className={cn("hover:text-brand-accent transition-colors", webSearch && "text-brand-accent")}
|
||||
>
|
||||
<Globe size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center mt-4">
|
||||
<p className="text-[9px] text-concrete/40 uppercase tracking-[0.3em] font-bold">Shift+Enter for new line</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</aside>
|
||||
|
||||
{autoLabelOpen && notebookId && (
|
||||
|
||||
Reference in New Issue
Block a user