diff --git a/memento-note/app/(admin)/admin/ai-test/ai-tester.tsx b/memento-note/app/(admin)/admin/ai-test/ai-tester.tsx index 2a20eeb..226c2bc 100644 --- a/memento-note/app/(admin)/admin/ai-test/ai-tester.tsx +++ b/memento-note/app/(admin)/admin/ai-test/ai-tester.tsx @@ -16,11 +16,12 @@ interface TestResult { tags?: Array<{ tag: string; confidence: number }> embeddingLength?: number firstValues?: number[] + chatResponse?: string error?: string details?: any } -export function AI_TESTER({ type }: { type: 'tags' | 'embeddings' }) { +export function AI_TESTER({ type }: { type: 'tags' | 'embeddings' | 'chat' }) { const { t } = useLanguage() const [isLoading, setIsLoading] = useState(false) const [result, setResult] = useState(null) @@ -99,6 +100,11 @@ export function AI_TESTER({ type }: { type: 'tags' | 'embeddings' }) { provider: config.AI_PROVIDER_TAGS || 'ollama', model: config.AI_MODEL_TAGS || 'granite4:latest' } + } else if (type === 'chat') { + return { + provider: config.AI_PROVIDER_CHAT || config.AI_PROVIDER_TAGS || 'ollama', + model: config.AI_MODEL_CHAT || 'granite4:latest' + } } else { return { provider: config.AI_PROVIDER_EMBEDDING || 'ollama', @@ -198,6 +204,19 @@ export function AI_TESTER({ type }: { type: 'tags' | 'embeddings' }) { )} + {/* Chat Response */} + {type === 'chat' && result.success && result.chatResponse && ( +
+
+ + Réponse du modèle +
+
+

"{result.chatResponse}"

+
+
+ )} + {/* Embeddings Results */} {type === 'embeddings' && result.success && result.embeddingLength && (
diff --git a/memento-note/app/(admin)/admin/ai-test/page.tsx b/memento-note/app/(admin)/admin/ai-test/page.tsx index d520d92..70b7304 100644 --- a/memento-note/app/(admin)/admin/ai-test/page.tsx +++ b/memento-note/app/(admin)/admin/ai-test/page.tsx @@ -30,7 +30,7 @@ export default function AITestPage() {
-
+
{/* Tags Provider Test */} @@ -62,8 +62,25 @@ export default function AITestPage() { + + {/* Chat Provider Test */} + + + + 💬 + Fournisseur de chat + + + Testez le fournisseur IA responsable de l'assistant conversationnel + + + + + +
+ {/* Info Section */} diff --git a/memento-note/app/(admin)/admin/ai/admin-ai-client.tsx b/memento-note/app/(admin)/admin/ai/admin-ai-client.tsx new file mode 100644 index 0000000..b340f40 --- /dev/null +++ b/memento-note/app/(admin)/admin/ai/admin-ai-client.tsx @@ -0,0 +1,209 @@ +'use client' + +import { useState, useEffect } from 'react' +import { getAISettings, updateAISettings } from '@/app/actions/ai-settings' +import { getSystemConfig } from '@/lib/config' +import { Switch } from '@/components/ui/switch' +import { Badge } from '@/components/ui/badge' +import { Zap, TrendingUp, Activity, Settings, Brain, Tag, Globe, Sparkles } from 'lucide-react' +import { AdminMetrics } from '@/components/admin-metrics' +import Link from 'next/link' +import { Button } from '@/components/ui/button' +import { toast } from 'sonner' +import { useLanguage } from '@/lib/i18n' + +interface FeatureState { + titleSuggestions: boolean + semanticSearch: boolean + paragraphRefactor: boolean + memoryEcho: boolean + languageDetection: boolean + autoLabeling: boolean +} + +interface ProviderInfo { + name: string + status: string +} + +export function AdminAIPageClient({ + initialFeatures, + providers, +}: { + initialFeatures: FeatureState + providers: ProviderInfo[] +}) { + const [features, setFeatures] = useState(initialFeatures) + const [saving, setSaving] = useState(null) + const { t } = useLanguage() + + const handleToggle = async (key: keyof FeatureState, value: boolean) => { + setSaving(key) + setFeatures(prev => ({ ...prev, [key]: value })) + try { + await updateAISettings({ [key]: value }) + toast.success(t('admin.ai.settingUpdated')) + } catch { + toast.error(t('admin.ai.updateFailedShort')) + setFeatures(prev => ({ ...prev, [key]: !value })) + } finally { + setSaving(null) + } + } + + const featureList = [ + { + key: 'titleSuggestions' as const, + label: t('admin.ai.titleSuggestions'), + description: t('admin.ai.titleSuggestionsDesc'), + icon: , + }, + + { + key: 'paragraphRefactor' as const, + label: t('admin.ai.aiAssistant'), + description: t('admin.ai.aiAssistantDesc'), + icon: , + }, + + { + key: 'memoryEcho' as const, + label: t('admin.ai.memoryEchoFeature'), + description: t('admin.ai.memoryEchoFeatureDesc'), + icon: , + }, + { + key: 'languageDetection' as const, + label: t('admin.ai.languageDetection'), + description: t('admin.ai.languageDetectionDesc'), + icon: , + }, + { + key: 'autoLabeling' as const, + label: t('admin.ai.autoLabeling'), + description: t('admin.ai.autoLabelingDesc'), + icon: , + }, + ] + + const aiMetrics = [ + { + title: t('admin.ai.activeFeatures'), + value: String(Object.values(features).filter(Boolean).length) + ' / ' + featureList.length, + trend: { value: 0, isPositive: true }, + icon: , + }, + { + title: t('admin.ai.successRate'), + value: '100%', + trend: { value: 0, isPositive: true }, + icon: , + }, + { + title: t('admin.ai.avgResponseTime'), + value: '—', + trend: { value: 0, isPositive: true }, + icon: , + }, + { + title: t('admin.ai.configuredProviders'), + value: String(providers.filter(p => p.status !== 'Not Configured').length), + icon: , + }, + ] + + return ( +
+
+
+

+ {t('admin.ai.pageTitle')} +

+

+ {t('admin.ai.pageDescription')} +

+
+ + + +
+ + + +
+ {/* Feature Toggles */} +
+

+ {t('admin.ai.features')} +

+
+ {featureList.map(({ key, label, description, icon }) => ( +
+
+ {icon} +
+

+ {label} +

+

+ {description} +

+
+
+ handleToggle(key, v)} + disabled={saving === key} + className="ml-3 flex-shrink-0" + /> +
+ ))} +
+
+ + {/* AI Provider Status */} +
+

+ {t('admin.ai.providerStatus')} +

+
+ {providers.map((provider) => ( +
+

+ {provider.name} +

+ + {provider.status} + +
+ ))} +
+
+
+ +
+

+ {t('admin.ai.recentRequests')} +

+

+ {t('admin.ai.comingSoon')} +

+
+
+ ) +} diff --git a/memento-note/app/(admin)/admin/ai/page.tsx b/memento-note/app/(admin)/admin/ai/page.tsx index 78b3b9d..4809503 100644 --- a/memento-note/app/(admin)/admin/ai/page.tsx +++ b/memento-note/app/(admin)/admin/ai/page.tsx @@ -1,12 +1,16 @@ -import { AdminMetrics } from '@/components/admin-metrics' -import { Button } from '@/components/ui/button' -import { Zap, Settings, Activity, TrendingUp } from 'lucide-react' import { getSystemConfig } from '@/lib/config' +import { getAISettings } from '@/app/actions/ai-settings' +import { auth } from '@/auth' +import { redirect } from 'next/navigation' +import { AdminAIPageClient } from './admin-ai-client' export default async function AdminAIPage() { - const config = await getSystemConfig() + const session = await auth() + if (!session?.user?.id) redirect('/api/auth/signin') + + const config = await getSystemConfig() + const settings = await getAISettings(session.user.id) - // Determine provider status based on config const openaiKey = config.OPENAI_API_KEY const ollamaUrl = config.OLLAMA_BASE_URL || config.OLLAMA_BASE_URL_TAGS || config.OLLAMA_BASE_URL_EMBEDDING @@ -14,133 +18,24 @@ export default async function AdminAIPage() { { name: 'OpenAI', status: openaiKey ? 'Connected' : 'Not Configured', - requests: 'N/A' // We don't track request counts yet }, { name: 'Ollama', status: ollamaUrl ? 'Available' : 'Not Configured', - requests: 'N/A' - }, - ] - - // Mock AI metrics - in a real app, these would come from analytics - // TODO: Implement real analytics tracking - const aiMetrics = [ - { - title: 'Total Requests', - value: '—', - trend: { value: 0, isPositive: true }, - icon: , - }, - { - title: 'Success Rate', - value: '100%', - trend: { value: 0, isPositive: true }, - icon: , - }, - { - title: 'Avg Response Time', - value: '—', - trend: { value: 0, isPositive: true }, - icon: , - }, - { - title: 'Active Features', - value: '6', - icon: , }, ] return ( -
-
-
-

- AI Management -

-

- Monitor and configure AI features -

-
- - - -
- - - -
-
-

- Active AI Features -

-
- {[ - 'Title Suggestions', - 'Semantic Search', - 'Paragraph Reformulation', - 'Memory Echo', - 'Language Detection', - 'Auto Labeling', - ].map((feature) => ( -
- - {feature} - - - Active - -
- ))} -
-
- -
-

- AI Provider Status -

-
- {providers.map((provider) => ( -
-
-

- {provider.name} -

-

- {provider.requests} requests -

-
- - {provider.status} - -
- ))} -
-
-
- -
-

- Recent AI Requests -

-

- Recent AI requests will be displayed here. (Coming Soon) -

-
-
+ ) } diff --git a/memento-note/app/(admin)/admin/layout.tsx b/memento-note/app/(admin)/admin/layout.tsx index 82807fc..42b9338 100644 --- a/memento-note/app/(admin)/admin/layout.tsx +++ b/memento-note/app/(admin)/admin/layout.tsx @@ -11,9 +11,9 @@ export default function AdminLayout({ children: React.ReactNode }) { return ( -
+
-
+
{children}
diff --git a/memento-note/app/(main)/agents/agents-page-client.tsx b/memento-note/app/(main)/agents/agents-page-client.tsx index 2d4873c..26285a4 100644 --- a/memento-note/app/(main)/agents/agents-page-client.tsx +++ b/memento-note/app/(main)/agents/agents-page-client.tsx @@ -6,7 +6,7 @@ */ import { useState, useCallback, useMemo, useEffect, useRef } from 'react' -import { Plus, Bot, LifeBuoy, Search } from 'lucide-react' +import { Plus, Bot, LayoutTemplate, Search, HelpCircle } from 'lucide-react' import { toast } from 'sonner' import { useLanguage } from '@/lib/i18n' @@ -79,6 +79,7 @@ export function AgentsPageClient({ const [showHelp, setShowHelp] = useState(false) const [searchQuery, setSearchQuery] = useState('') const [typeFilter, setTypeFilter] = useState('') + const [activeTab, setActiveTab] = useState<'dashboard' | 'templates'>('dashboard') const refreshAgents = useCallback(async () => { try { @@ -90,20 +91,15 @@ export function AgentsPageClient({ } }, []) - // Track latest action ID per agent to detect new executions const prevActionsRef = useRef>({}) - // Check for new actions and show toast notifications const checkForNewActions = useCallback((updated: AgentItem[]) => { for (const agent of updated) { const lastAction = agent.actions[0] if (!lastAction) continue - const prevId = prevActionsRef.current[agent.id] - if (prevId === undefined) continue // Not tracked - + if (prevId === undefined) continue if (prevId !== lastAction.id) { - // Only toast for recently created actions (within 5 min) const age = Date.now() - new Date(lastAction.createdAt).getTime() if (age < 5 * 60 * 1000) { if (lastAction.status === 'success') { @@ -118,18 +114,13 @@ export function AgentsPageClient({ }, [t]) useEffect(() => { - // Initialize tracking from initial data for (const agent of initialAgents) { prevActionsRef.current[agent.id] = agent.actions[0]?.id ?? null } - - // Interval polling every 30s const interval = setInterval(async () => { const updated = await refreshAgents() if (updated) checkForNewActions(updated) }, 30000) - - // Refresh immediately when user comes back to the tab const onVisible = async () => { if (document.visibilityState === 'visible') { const updated = await refreshAgents() @@ -137,7 +128,6 @@ export function AgentsPageClient({ } } document.addEventListener('visibilitychange', onVisible) - return () => { clearInterval(interval) document.removeEventListener('visibilitychange', onVisible) @@ -179,7 +169,6 @@ export function AgentsPageClient({ scheduledDay: formData.get('scheduledDay') ? Number(formData.get('scheduledDay')) : undefined, timezone: (formData.get('timezone') as string) || undefined, } - if (editingAgent) { await updateAgent(editingAgent.id, data) toast.success(t('agents.toasts.updated')) @@ -187,7 +176,6 @@ export function AgentsPageClient({ await createAgent(data) toast.success(t('agents.toasts.created')) } - setShowForm(false) setEditingAgent(null) await refreshAgents() @@ -208,110 +196,142 @@ export function AgentsPageClient({ const existingAgentNames = useMemo(() => agents.map(a => a.name), [agents]) return ( - <> - {/* Header */} -
-
-
- -
-
-

{t('agents.title')}

-

{t('agents.subtitle')}

+ /* Full-bleed layout: -m-4 cancels the p-4 of the parent
*/ +
+ + {/* ── LEFT SIDEBAR ── */} +
- {/* Action buttons */} -
- - -
- - {/* Agents grid */} - {agents.length > 0 && ( -
-

+ {/* Nav */} +

+ + - {/* Search and filter */} -
-
- - setSearchQuery(e.target.value)} - placeholder={t('agents.searchPlaceholder')} - className="w-full pl-9 pr-3 py-2 text-sm bg-card border border-border rounded-lg outline-none focus:border-primary/40 focus:ring-2 focus:ring-primary/10 transition-all" - /> -
-
- {typeFilterOptions.map(opt => ( - - ))} -
+ {/* Footer: Help */} +
+ +
+ + + {/* ── MAIN CONTENT ── */} +
+ + {/* Top header bar */} +
+
+

+ {t('agents.myAgents')} +

+

+ {t('agents.subtitle')} +

+ +
- {filteredAgents.length > 0 ? ( -
- {filteredAgents.map(agent => ( - - ))} -
- ) : ( -
- -

{t('agents.noResults')}

-
+ {/* Scrollable content area */} +
+ + {/* Dashboard tab - agents + templates */} + {activeTab === 'dashboard' && ( + <> + {agents.length > 0 && ( + <> + {/* Filter pills + search */} +
+
+ {typeFilterOptions.map(opt => ( + + ))} +
+
+ + setSearchQuery(e.target.value)} + placeholder={t('agents.searchPlaceholder')} + className="pl-9 pr-4 py-2 text-[13px] bg-card border border-border/50 rounded-lg outline-none focus:border-primary/50 focus:ring-2 focus:ring-primary/10 transition-all placeholder:text-muted-foreground/40 w-56" + /> +
+
+ + {filteredAgents.length > 0 ? ( +
+ {filteredAgents.map(agent => ( + + ))} +
+ ) : ( +
+ +

{t('agents.noResults')}

+
+ )} + + )} + + {agents.length === 0 && ( +
+ +

{t('agents.noAgents')}

+

{t('agents.noAgentsDescription')}

+
+ )} + + {/* Templates always visible on dashboard */} + + )} -
- )} +
+
- {/* Empty state */} - {agents.length === 0 && ( -
- -

{t('agents.noAgents')}

-

- {t('agents.noAgentsDescription')} -

-
- )} - - {/* Templates */} - - - {/* Form modal */} + {/* Sliding panels */} {showForm && ( { setShowForm(false); setEditingAgent(null) }} /> )} - - {/* Run log modal */} {logAgent && ( setLogAgent(null)} /> )} - - {/* Help modal */} {showHelp && ( setShowHelp(false)} /> )} - +
) } diff --git a/memento-note/app/(main)/agents/page.tsx b/memento-note/app/(main)/agents/page.tsx index 489782c..5f99b81 100644 --- a/memento-note/app/(main)/agents/page.tsx +++ b/memento-note/app/(main)/agents/page.tsx @@ -19,15 +19,9 @@ export default async function AgentsPage() { ]) return ( -
-
-
- -
-
-
+ ) } diff --git a/memento-note/app/(main)/layout.tsx b/memento-note/app/(main)/layout.tsx index 11ce662..726ead3 100644 --- a/memento-note/app/(main)/layout.tsx +++ b/memento-note/app/(main)/layout.tsx @@ -5,6 +5,9 @@ import { ProvidersWrapper } from "@/components/providers-wrapper"; import { auth } from "@/auth"; import { detectUserLanguage } from "@/lib/i18n/detect-user-language"; import { loadTranslations } from "@/lib/i18n/load-translations"; +import { getAISettings } from "@/app/actions/ai-settings"; + +import { AIChat } from "@/components/ai-chat"; export default async function MainLayout({ children, @@ -20,6 +23,12 @@ export default async function MainLayout({ // Load initial translations server-side to prevent hydration mismatch const initialTranslations = await loadTranslations(initialLanguage); + // Load AI settings to conditionally render AI features + const aiSettings = session?.user?.id + ? await getAISettings(session.user.id) + : null; + const showAIAssistant = aiSettings?.paragraphRefactor !== false; + return (
@@ -27,7 +36,7 @@ export default async function MainLayout({ {/* Main Layout */} -
+
{/* Sidebar Navigation - Style Keep */} }> @@ -37,8 +46,12 @@ export default async function MainLayout({
{children}
+ + {/* AI Chat Drawer — only shown if user has Assistant IA enabled */} + {showAIAssistant && }
); } + diff --git a/memento-note/app/actions/ai-settings.ts b/memento-note/app/actions/ai-settings.ts index 9d3665f..c8b1c8e 100644 --- a/memento-note/app/actions/ai-settings.ts +++ b/memento-note/app/actions/ai-settings.ts @@ -19,6 +19,8 @@ export type UserAISettingsData = { desktopNotifications?: boolean anonymousAnalytics?: boolean fontSize?: 'small' | 'medium' | 'large' + languageDetection?: boolean + autoLabeling?: boolean } /** Only fields that exist on `UserAISettings` in Prisma (excludes e.g. `theme`, which lives on `User`). */ @@ -37,6 +39,8 @@ const USER_AI_SETTINGS_PRISMA_KEYS = [ 'emailNotifications', 'desktopNotifications', 'anonymousAnalytics', + 'languageDetection', + 'autoLabeling', ] as const type UserAISettingsPrismaKey = (typeof USER_AI_SETTINGS_PRISMA_KEYS)[number] @@ -144,7 +148,9 @@ const getCachedAISettings = unstable_cache( desktopNotifications: false, anonymousAnalytics: false, theme: 'light' as const, - fontSize: 'medium' as const + fontSize: 'medium' as const, + languageDetection: true, + autoLabeling: true, } } @@ -171,7 +177,9 @@ const getCachedAISettings = unstable_cache( desktopNotifications: settings.desktopNotifications, anonymousAnalytics: settings.anonymousAnalytics, // theme: 'light' as const, // REMOVED: Should not be handled here or hardcoded - fontSize: (settings.fontSize || 'medium') as 'small' | 'medium' | 'large' + fontSize: (settings.fontSize || 'medium') as 'small' | 'medium' | 'large', + languageDetection: settings.languageDetection ?? true, + autoLabeling: settings.autoLabeling ?? true, } } catch (error) { console.error('Error getting AI settings:', error) diff --git a/memento-note/app/actions/notes.ts b/memento-note/app/actions/notes.ts index f4bc508..2ea9d0a 100644 --- a/memento-note/app/actions/notes.ts +++ b/memento-note/app/actions/notes.ts @@ -9,6 +9,8 @@ import { parseNote as parseNoteUtil, cosineSimilarity, calculateRRFK, detectQuer import { getSystemConfig, getConfigNumber, getConfigBoolean, SEARCH_DEFAULTS } from '@/lib/config' import { contextualAutoTagService } from '@/lib/ai/services/contextual-auto-tag.service' import { cleanupNoteImages, parseImageUrls, deleteImageFileSafely } from '@/lib/image-cleanup' +import { getAISettings } from '@/app/actions/ai-settings' + /** * Champs sélectionnés pour les listes de notes (sans embedding pour économiser ~6KB/note). @@ -490,7 +492,8 @@ export async function createNote(data: { // Background task 2: Auto-labeling (only if no user labels and has notebook) if (!hasUserLabels && notebookId) { try { - const autoLabelingEnabled = await getConfigBoolean('AUTO_LABELING_ENABLED', true) + const userAISettings = await getAISettings(userId) + const autoLabelingEnabled = userAISettings.autoLabeling !== false const autoLabelingConfidence = await getConfigNumber('AUTO_LABELING_CONFIDENCE_THRESHOLD', 70) console.log('[BG] Auto-labeling check: enabled=', autoLabelingEnabled, 'confidence=', autoLabelingConfidence, 'notebookId=', notebookId) @@ -671,7 +674,7 @@ export async function updateNote(id: string, data: { } // Soft-delete a note (move to trash) -export async function deleteNote(id: string) { +export async function deleteNote(id: string, options?: { skipRevalidation?: boolean }) { const session = await auth(); if (!session?.user?.id) throw new Error('Unauthorized'); @@ -681,7 +684,9 @@ export async function deleteNote(id: string) { data: { trashedAt: new Date() } }) - revalidatePath('/') + if (!options?.skipRevalidation) { + revalidatePath('/') + } return { success: true } } catch (error) { console.error('Error deleting note:', error) @@ -690,7 +695,7 @@ export async function deleteNote(id: string) { } // Trash actions -export async function trashNote(id: string) { +export async function trashNote(id: string, options?: { skipRevalidation?: boolean }) { const session = await auth(); if (!session?.user?.id) throw new Error('Unauthorized'); @@ -699,7 +704,9 @@ export async function trashNote(id: string) { where: { id, userId: session.user.id }, data: { trashedAt: new Date() } }) - revalidatePath('/') + if (!options?.skipRevalidation) { + revalidatePath('/') + } return { success: true } } catch (error) { console.error('Error trashing note:', error) diff --git a/memento-note/app/api/ai/auto-labels/route.ts b/memento-note/app/api/ai/auto-labels/route.ts index 7deba00..9a5e475 100644 --- a/memento-note/app/api/ai/auto-labels/route.ts +++ b/memento-note/app/api/ai/auto-labels/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' import { auth } from '@/auth' import { autoLabelCreationService } from '@/lib/ai/services' +import { getAISettings } from '@/app/actions/ai-settings' /** * POST /api/ai/auto-labels - Suggest new labels for a notebook @@ -16,6 +17,12 @@ export async function POST(request: NextRequest) { ) } + // Respect user's autoLabeling toggle + const userSettings = await getAISettings(session.user.id) + if (userSettings.autoLabeling === false) { + return NextResponse.json({ success: true, data: null, message: 'Auto-labeling disabled by user' }) + } + const body = await request.json() const { notebookId, language = 'en' } = body diff --git a/memento-note/app/api/ai/config/route.ts b/memento-note/app/api/ai/config/route.ts index c696298..4d7b4e7 100644 --- a/memento-note/app/api/ai/config/route.ts +++ b/memento-note/app/api/ai/config/route.ts @@ -10,6 +10,8 @@ export async function GET(request: NextRequest) { AI_MODEL_TAGS: config.AI_MODEL_TAGS || 'not set', AI_PROVIDER_EMBEDDING: config.AI_PROVIDER_EMBEDDING || 'not set', AI_MODEL_EMBEDDING: config.AI_MODEL_EMBEDDING || 'not set', + AI_PROVIDER_CHAT: config.AI_PROVIDER_CHAT || 'not set', + AI_MODEL_CHAT: config.AI_MODEL_CHAT || 'not set', OPENAI_API_KEY: config.OPENAI_API_KEY ? '***configured***' : '', CUSTOM_OPENAI_API_KEY: config.CUSTOM_OPENAI_API_KEY ? '***configured***' : '', CUSTOM_OPENAI_BASE_URL: config.CUSTOM_OPENAI_BASE_URL || '', diff --git a/memento-note/app/api/ai/reformulate/route.ts b/memento-note/app/api/ai/reformulate/route.ts index 39c03af..e5e3c84 100644 --- a/memento-note/app/api/ai/reformulate/route.ts +++ b/memento-note/app/api/ai/reformulate/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' import { auth } from '@/auth' import { paragraphRefactorService } from '@/lib/ai/services/paragraph-refactor.service' +import { getAISettings } from '@/app/actions/ai-settings' export async function POST(request: NextRequest) { try { @@ -10,6 +11,12 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } + // Respect user's paragraphRefactor toggle (Assistant IA) + const userSettings = await getAISettings(session.user.id) + if (userSettings.paragraphRefactor === false) { + return NextResponse.json({ error: 'Feature disabled' }, { status: 403 }) + } + const { text, option } = await request.json() // Validation diff --git a/memento-note/app/api/ai/tags/route.ts b/memento-note/app/api/ai/tags/route.ts index 1f681f7..217feb8 100644 --- a/memento-note/app/api/ai/tags/route.ts +++ b/memento-note/app/api/ai/tags/route.ts @@ -5,6 +5,8 @@ import { getTagsProvider } from '@/lib/ai/factory'; import { getSystemConfig } from '@/lib/config'; import { z } from 'zod'; +import { getAISettings } from '@/app/actions/ai-settings'; + const requestSchema = z.object({ content: z.string().min(1, "Le contenu ne peut pas être vide"), notebookId: z.string().optional(), @@ -18,6 +20,11 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } + const userSettings = await getAISettings(session.user.id); + if (userSettings.autoLabeling === false) { + return NextResponse.json({ tags: [] }); + } + const body = await req.json(); const { content, notebookId, language } = requestSchema.parse(body); diff --git a/memento-note/app/api/ai/test-chat/route.ts b/memento-note/app/api/ai/test-chat/route.ts new file mode 100644 index 0000000..196b2ec --- /dev/null +++ b/memento-note/app/api/ai/test-chat/route.ts @@ -0,0 +1,46 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getChatProvider } from '@/lib/ai/factory' +import { getSystemConfig } from '@/lib/config' + +export async function POST(request: NextRequest) { + try { + const config = await getSystemConfig() + const provider = getChatProvider(config) + + const testMessage = 'Réponds en exactement 3 mots : quel est ton nom ?' + + const startTime = Date.now() + const response = await provider.generateText(testMessage) + const endTime = Date.now() + + if (!response || response.trim().length === 0) { + return NextResponse.json( + { + success: false, + error: 'No response from chat provider', + model: config.AI_MODEL_CHAT || 'granite4:latest', + }, + { status: 500 } + ) + } + + return NextResponse.json({ + success: true, + model: config.AI_MODEL_CHAT || 'granite4:latest', + chatResponse: response.trim(), + responseTime: endTime - startTime, + }) + } catch (error: any) { + const config = await getSystemConfig() + + return NextResponse.json( + { + success: false, + error: error.message || 'Unknown error', + model: config.AI_MODEL_CHAT || 'granite4:latest', + stack: process.env.NODE_ENV === 'development' ? error.stack : undefined, + }, + { status: 500 } + ) + } +} diff --git a/memento-note/app/api/ai/title-suggestions/route.ts b/memento-note/app/api/ai/title-suggestions/route.ts index 901566e..13b7a73 100644 --- a/memento-note/app/api/ai/title-suggestions/route.ts +++ b/memento-note/app/api/ai/title-suggestions/route.ts @@ -1,6 +1,8 @@ import { NextRequest, NextResponse } from 'next/server' import { getTagsProvider } from '@/lib/ai/factory' import { getSystemConfig } from '@/lib/config' +import { auth } from '@/auth' +import { getAISettings } from '@/app/actions/ai-settings' import { z } from 'zod' const requestSchema = z.object({ @@ -9,6 +11,16 @@ const requestSchema = z.object({ export async function POST(req: NextRequest) { try { + // Check authentication and user setting + const session = await auth() + if (session?.user?.id) { + const settings = await getAISettings(session.user.id) + if (settings.titleSuggestions === false) { + return NextResponse.json({ suggestions: [] }) + } + } + + const body = await req.json() const { content } = requestSchema.parse(body) diff --git a/memento-note/app/api/ai/web-search-available/route.ts b/memento-note/app/api/ai/web-search-available/route.ts new file mode 100644 index 0000000..52d0a8e --- /dev/null +++ b/memento-note/app/api/ai/web-search-available/route.ts @@ -0,0 +1,17 @@ +import { NextResponse } from 'next/server' +import { getSystemConfig } from '@/lib/config' + +export async function GET() { + try { + const config = await getSystemConfig() + const available = !!( + config.WEB_SEARCH_PROVIDER || + config.BRAVE_SEARCH_API_KEY || + config.SEARXNG_URL || + config.JINA_API_KEY + ) + return NextResponse.json({ available }) + } catch { + return NextResponse.json({ available: false }) + } +} diff --git a/memento-note/app/api/chat/history/route.ts b/memento-note/app/api/chat/history/route.ts new file mode 100644 index 0000000..51c8e97 --- /dev/null +++ b/memento-note/app/api/chat/history/route.ts @@ -0,0 +1,28 @@ +import { NextResponse } from 'next/server' +import { auth } from '@/auth' +import prisma from '@/lib/prisma' + +export async function GET(req: Request) { + try { + const session = await auth() + if (!session?.user?.id) { + return new NextResponse('Unauthorized', { status: 401 }) + } + + const conversations = await prisma.conversation.findMany({ + where: { userId: session.user.id }, + orderBy: { updatedAt: 'desc' }, + take: 5, + include: { + messages: { + orderBy: { createdAt: 'asc' } + } + } + }) + + return NextResponse.json(conversations) + } catch (error) { + console.error('Error fetching chat history:', error) + return new NextResponse('Internal Server Error', { status: 500 }) + } +} diff --git a/memento-note/app/api/chat/insights/route.ts b/memento-note/app/api/chat/insights/route.ts new file mode 100644 index 0000000..be3f34c --- /dev/null +++ b/memento-note/app/api/chat/insights/route.ts @@ -0,0 +1,44 @@ +import { NextResponse } from 'next/server' +import { auth } from '@/auth' +import { prisma } from '@/lib/prisma' +import { generateText } from 'ai' +import { getChatProvider } from '@/lib/ai/factory' +import { getSystemConfig } from '@/lib/config' + +export async function GET(req: Request) { + try { + const session = await auth() + if (!session?.user?.id) { + return new NextResponse('Unauthorized', { status: 401 }) + } + + // Fetch the 5 most recently updated notes + const notes = await prisma.note.findMany({ + where: { userId: session.user.id }, + orderBy: { updatedAt: 'desc' }, + take: 5, + select: { title: true, content: true } + }) + + if (notes.length === 0) { + return NextResponse.json({ insight: "Vous n'avez pas encore de notes pour générer des insights." }) + } + + const context = notes.map((n, i) => `Note ${i + 1}:\nTitre: ${n.title || 'Sans titre'}\nContenu: ${n.content}`).join('\n\n') + const config = await getSystemConfig() + const provider = getChatProvider(config) + const model = provider.getModel() + + const result = await generateText({ + model, + system: `Tu es un assistant IA d'analyse. Ton but est de lire les 5 dernières notes de l'utilisateur et d'en dégager un résumé synthétique ou 3 insights (idées clés/tendances). + Sois concis, direct, et réponds en markdown. Le ton doit être professionnel.`, + prompt: `Voici les 5 dernières notes de l'utilisateur:\n\n${context}` + }) + + return NextResponse.json({ insight: result.text }) + } catch (error) { + console.error('Error generating insights:', error) + return new NextResponse('Internal Server Error', { status: 500 }) + } +} diff --git a/memento-note/app/api/chat/route.ts b/memento-note/app/api/chat/route.ts index 7bd50c9..432fa45 100644 --- a/memento-note/app/api/chat/route.ts +++ b/memento-note/app/api/chat/route.ts @@ -47,12 +47,13 @@ export async function POST(req: Request) { // 2. Parse request body — messages arrive as UIMessage[] from DefaultChatTransport const body = await req.json() - const { messages: rawMessages, conversationId, notebookId, language, webSearch } = body as { + const { messages: rawMessages, conversationId, notebookId, language, webSearch, noteContext } = body as { messages: UIMessage[] conversationId?: string notebookId?: string language?: string webSearch?: boolean + noteContext?: { title: string; content: string; tone: string } } // Convert UIMessages to CoreMessages for streamText @@ -146,7 +147,16 @@ export async function POST(req: Request) { - Natural tone, neither corporate nor too casual. - No unnecessary intro phrases ("Here's what I found", "Based on your notes"). Answer directly. - No upsell questions at the end ("Would you like me to...", "Do you want..."). If you have useful additional info, just give it. -- If the user says "Momento" they mean Memento (this app). +- If the user says "Momento" they mean Momento (this app). + +## About Momento +Momento is an intelligent note-taking application. Key features include: +- **Notes & Editor**: Create rich Markdown notes with an integrated AI Copilot to rewrite, summarize, or translate content. +- **Organization**: Group notes into Notebooks and tag them with Labels. +- **Search**: Advanced semantic search to find notes by meaning, not just keywords, and Web Search integration. +- **Agents**: Create specialized AI Agents with custom system prompts for specific recurring tasks. +- **Lab**: Experimental AI tools for data analysis and deeper insights. +If the user asks how to use this tool, explain these features simply and helpfully. ## Available tools You have access to these tools for deeper research: @@ -174,7 +184,16 @@ You have access to these tools for deeper research: - Ton naturel, ni corporate ni trop familier. - Pas de phrase d'intro inutile ("Voici ce que j'ai trouvé", "Basé sur vos notes"). Réponds directement. - Pas de question upsell à la fin ("Souhaitez-vous que je...", "Acceptez-vous que..."). Si tu as une info complémentaire utile, donne-la. -- Si l'utilisateur dit "Momento" il parle de Memento (cette application). +- Si l'utilisateur dit "Momento" il parle de Momento (cette application). + +## À propos de Momento +Momento est une application de prise de notes intelligente. Ses fonctionnalités principales : +- **Éditeur de notes** : Prise de notes en Markdown riche avec un Copilot IA intégré pour réécrire, résumer ou traduire du texte. +- **Organisation** : Regroupement des notes dans des Carnets (Notebooks) et utilisation d'Étiquettes (Labels). +- **Recherche** : Recherche sémantique avancée pour trouver des notes par le sens, et recherche Web intégrée. +- **Agents** : Création d'Agents IA spécialisés avec des instructions personnalisées pour des tâches récurrentes. +- **Lab** : Outils IA expérimentaux pour l'analyse de données et les insights. +Si l'utilisateur demande comment utiliser cet outil, explique ces fonctionnalités simplement et avec bienveillance. ## Outils disponibles Tu as accès à ces outils pour des recherches approfondies : @@ -301,7 +320,21 @@ Tu as accès à ces outils pour des recherches approfondies : ? prompts.contextWithNotes : prompts.contextNoNotes + let copilotContext = '' + if (noteContext) { + copilotContext = `\n\n## Current Note Context +You are currently helping the user edit a specific note. Here is the current content of the note: +Title: ${noteContext.title || 'Untitled'} + +Content: +${noteContext.content || '(empty)'} + +The user wants you to write in a **${noteContext.tone || 'professional'}** tone. +Keep your suggestions tailored to this note and tone. You can suggest rewrites, answer questions about the note, or draft new sections.` + } + const systemPrompt = `${prompts.system} +${copilotContext} ${contextBlock} diff --git a/memento-note/app/globals.css b/memento-note/app/globals.css index 719c61f..29b7dfb 100644 --- a/memento-note/app/globals.css +++ b/memento-note/app/globals.css @@ -16,6 +16,12 @@ --color-primary: #64748b; --color-background-light: #f7f7f8; --color-background-dark: #1a1d23; + + /* Stitch Design Tokens */ + --font-sans: var(--font-inter); + --font-heading: var(--font-manrope); + --shadow-level-1: 0px 4px 20px rgba(15, 23, 42, 0.05); + --shadow-level-2: 0px 10px 30px rgba(15, 23, 42, 0.08); } /* Custom scrollbar for better aesthetics */ @@ -98,25 +104,25 @@ } :root { - --radius: 0.625rem; - --background: oklch(0.985 0.003 230); /* Blanc grisâtre */ + --radius: 0.5rem; + --background: #f8fafc; /* Sub-surface off-white */ --foreground: oklch(0.2 0.02 230); /* Gris-bleu foncé */ --card: oklch(1 0 0); /* Blanc pur */ --card-foreground: oklch(0.2 0.02 230); --popover: oklch(1 0 0); --popover-foreground: oklch(0.2 0.02 230); - --primary: oklch(0.45 0.08 230); /* Gris-bleu doux */ - --primary-foreground: oklch(0.99 0 0); /* Blanc */ - --secondary: oklch(0.94 0.005 230); /* Gris-bleu très pâle */ - --secondary-foreground: oklch(0.2 0.02 230); - --muted: oklch(0.92 0.005 230); - --muted-foreground: oklch(0.6 0.01 230); - --accent: oklch(0.94 0.005 230); - --accent-foreground: oklch(0.2 0.02 230); + --primary: #0284c7; /* Sky Blue */ + --primary-foreground: #ffffff; /* Blanc */ + --secondary: #e2e8f0; /* Gris-bleu très pâle */ + --secondary-foreground: #1e293b; + --muted: #f1f5f9; + --muted-foreground: #64748b; + --accent: #f8fafc; + --accent-foreground: #0284c7; --destructive: oklch(0.6 0.18 25); /* Rouge */ - --border: oklch(0.9 0.008 230); /* Gris-bleu très clair */ - --input: oklch(0.98 0.003 230); - --ring: oklch(0.7 0.005 230); + --border: #e2e8f0; /* Gris-bleu très clair */ + --input: #ffffff; + --ring: #0284c7; --chart-1: oklch(0.646 0.222 41.116); --chart-2: oklch(0.6 0.118 184.704); --chart-3: oklch(0.398 0.07 227.392); diff --git a/memento-note/app/layout.tsx b/memento-note/app/layout.tsx index be06edf..dec534f 100644 --- a/memento-note/app/layout.tsx +++ b/memento-note/app/layout.tsx @@ -1,5 +1,4 @@ import type { Metadata, Viewport } from "next"; -import { Inter } from "next/font/google"; import "./globals.css"; import { Toaster } from "@/components/ui/toast"; import { SessionProviderWrapper } from "@/components/session-provider-wrapper"; @@ -9,9 +8,18 @@ import { ThemeInitializer } from "@/components/theme-initializer"; import { DirectionInitializer } from "@/components/direction-initializer"; import { ErrorReporter } from "@/components/error-reporter"; import { auth } from "@/auth"; +import Script from "next/script"; + +import { Inter, Manrope } from "next/font/google"; const inter = Inter({ subsets: ["latin"], + variable: "--font-inter", +}); + +const manrope = Manrope({ + subsets: ["latin"], + variable: "--font-manrope", }); export const metadata: Metadata = { @@ -71,10 +79,13 @@ export default async function RootLayout({ return ( - -