feat: Complete internationalization and code cleanup
## Translation Files - Add 11 new language files (es, de, pt, ru, zh, ja, ko, ar, hi, nl, pl) - Add 100+ missing translation keys across all 15 languages - New sections: notebook, pagination, ai.batchOrganization, ai.autoLabels - Update nav section with workspace, quickAccess, myLibrary keys ## Component Updates - Update 15+ components to use translation keys instead of hardcoded text - Components: notebook dialogs, sidebar, header, note-input, ghost-tags, etc. - Replace 80+ hardcoded English/French strings with t() calls - Ensure consistent UI across all supported languages ## Code Quality - Remove 77+ console.log statements from codebase - Clean up API routes, components, hooks, and services - Keep only essential error handling (no debugging logs) ## UI/UX Improvements - Update Keep logo to yellow post-it style (from-yellow-400 to-amber-500) - Change selection colors to #FEF3C6 (notebooks) and #EFB162 (nav items) - Make "+" button permanently visible in notebooks section - Fix grammar and syntax errors in multiple components ## Bug Fixes - Fix JSON syntax errors in it.json, nl.json, pl.json, zh.json - Fix syntax errors in notebook-suggestion-toast.tsx - Fix syntax errors in use-auto-tagging.ts - Fix syntax errors in paragraph-refactor.service.ts - Fix duplicate "fusion" section in nl.json 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> Ou une version plus courte si vous préférez : feat(i18n): Add 15 languages, remove logs, update UI components - Create 11 new translation files (es, de, pt, ru, zh, ja, ko, ar, hi, nl, pl) - Add 100+ translation keys: notebook, pagination, AI features - Update 15+ components to use translations (80+ strings) - Remove 77+ console.log statements from codebase - Fix JSON syntax errors in 4 translation files - Fix component syntax errors (toast, hooks, services) - Update logo to yellow post-it style - Change selection colors (#FEF3C6, #EFB162) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
121
keep-notes/app/api/ai/auto-labels/route.ts
Normal file
121
keep-notes/app/api/ai/auto-labels/route.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { autoLabelCreationService } from '@/lib/ai/services'
|
||||
|
||||
/**
|
||||
* POST /api/ai/auto-labels - Suggest new labels for a notebook
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { notebookId } = body
|
||||
|
||||
if (!notebookId || typeof notebookId !== 'string') {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Missing required field: notebookId' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check if notebook belongs to user
|
||||
const { prisma } = await import('@/lib/prisma')
|
||||
const notebook = await prisma.notebook.findFirst({
|
||||
where: {
|
||||
id: notebookId,
|
||||
userId: session.user.id,
|
||||
},
|
||||
})
|
||||
|
||||
if (!notebook) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Notebook not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get label suggestions
|
||||
const suggestions = await autoLabelCreationService.suggestLabels(
|
||||
notebookId,
|
||||
session.user.id
|
||||
)
|
||||
|
||||
if (!suggestions) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: null,
|
||||
message: 'No suggestions available (notebook may have fewer than 15 notes)',
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: suggestions,
|
||||
})
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to get label suggestions',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /api/ai/auto-labels - Create suggested labels
|
||||
*/
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { suggestions, selectedLabels } = body
|
||||
|
||||
if (!suggestions || !Array.isArray(selectedLabels)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Missing required fields: suggestions, selectedLabels' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Create labels
|
||||
const createdCount = await autoLabelCreationService.createLabels(
|
||||
suggestions.notebookId,
|
||||
session.user.id,
|
||||
suggestions,
|
||||
selectedLabels
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
createdCount,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to create labels',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
85
keep-notes/app/api/ai/batch-organize/route.ts
Normal file
85
keep-notes/app/api/ai/batch-organize/route.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { batchOrganizationService } from '@/lib/ai/services'
|
||||
|
||||
/**
|
||||
* POST /api/ai/batch-organize - Create organization plan for notes in Inbox
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Create organization plan
|
||||
const plan = await batchOrganizationService.createOrganizationPlan(
|
||||
session.user.id
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: plan,
|
||||
})
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to create organization plan',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /api/ai/batch-organize - Apply organization plan
|
||||
*/
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { plan, selectedNoteIds } = body
|
||||
|
||||
if (!plan || !Array.isArray(selectedNoteIds)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Missing required fields: plan, selectedNoteIds' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Apply organization plan
|
||||
const movedCount = await batchOrganizationService.applyOrganizationPlan(
|
||||
session.user.id,
|
||||
plan,
|
||||
selectedNoteIds
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
movedCount,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to apply organization plan',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ export async function GET(request: NextRequest) {
|
||||
OLLAMA_BASE_URL: config.OLLAMA_BASE_URL || 'http://localhost:11434'
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching AI config:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error.message || 'Failed to fetch config'
|
||||
|
||||
85
keep-notes/app/api/ai/echo/connections/route.ts
Normal file
85
keep-notes/app/api/ai/echo/connections/route.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { memoryEchoService } from '@/lib/ai/services/memory-echo.service'
|
||||
|
||||
/**
|
||||
* GET /api/ai/echo/connections?noteId={id}&page={page}&limit={limit}
|
||||
* Fetch all connections for a specific note
|
||||
*/
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get query parameters
|
||||
const { searchParams } = new URL(req.url)
|
||||
const noteId = searchParams.get('noteId')
|
||||
const page = parseInt(searchParams.get('page') || '1')
|
||||
const limit = parseInt(searchParams.get('limit') || '10')
|
||||
|
||||
// Validate noteId
|
||||
if (!noteId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'noteId parameter is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Validate pagination parameters
|
||||
if (page < 1 || limit < 1 || limit > 50) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid pagination parameters. page >= 1, limit between 1 and 50' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get all connections for the note
|
||||
const allConnections = await memoryEchoService.getConnectionsForNote(noteId, session.user.id)
|
||||
|
||||
// Calculate pagination
|
||||
const total = allConnections.length
|
||||
const startIndex = (page - 1) * limit
|
||||
const endIndex = startIndex + limit
|
||||
const paginatedConnections = allConnections.slice(startIndex, endIndex)
|
||||
|
||||
// Format connections for response
|
||||
const connections = paginatedConnections.map(conn => {
|
||||
// Determine which note is the "other" note (not the target note)
|
||||
const isNote1Target = conn.note1.id === noteId
|
||||
const otherNote = isNote1Target ? conn.note2 : conn.note1
|
||||
|
||||
return {
|
||||
noteId: otherNote.id,
|
||||
title: otherNote.title,
|
||||
content: otherNote.content,
|
||||
createdAt: otherNote.createdAt,
|
||||
similarity: conn.similarityScore,
|
||||
daysApart: conn.daysApart
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
connections,
|
||||
pagination: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
hasNext: endIndex < total,
|
||||
hasPrev: page > 1
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch connections' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
60
keep-notes/app/api/ai/echo/dismiss/route.ts
Normal file
60
keep-notes/app/api/ai/echo/dismiss/route.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
/**
|
||||
* POST /api/ai/echo/dismiss
|
||||
* Dismiss a connection for a specific note
|
||||
* Body: { noteId, connectedNoteId }
|
||||
*/
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await req.json()
|
||||
const { noteId, connectedNoteId } = body
|
||||
|
||||
if (!noteId || !connectedNoteId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'noteId and connectedNoteId are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Find and mark matching insights as dismissed
|
||||
// We need to find insights where (note1Id = noteId AND note2Id = connectedNoteId) OR (note1Id = connectedNoteId AND note2Id = noteId)
|
||||
await prisma.memoryEchoInsight.updateMany({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
OR: [
|
||||
{
|
||||
note1Id: noteId,
|
||||
note2Id: connectedNoteId
|
||||
},
|
||||
{
|
||||
note1Id: connectedNoteId,
|
||||
note2Id: noteId
|
||||
}
|
||||
]
|
||||
},
|
||||
data: {
|
||||
dismissed: true
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to dismiss connection' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
107
keep-notes/app/api/ai/echo/fusion/route.ts
Normal file
107
keep-notes/app/api/ai/echo/fusion/route.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { getAIProvider } from '@/lib/ai/factory'
|
||||
import prisma from '@/lib/prisma'
|
||||
|
||||
/**
|
||||
* POST /api/ai/echo/fusion
|
||||
* Generate intelligent fusion of multiple notes
|
||||
*/
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await req.json()
|
||||
const { noteIds, prompt } = body
|
||||
|
||||
if (!noteIds || !Array.isArray(noteIds) || noteIds.length < 2) {
|
||||
return NextResponse.json(
|
||||
{ error: 'At least 2 note IDs are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Fetch the notes
|
||||
const notes = await prisma.note.findMany({
|
||||
where: {
|
||||
id: { in: noteIds },
|
||||
userId: session.user.id
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
content: true,
|
||||
createdAt: true
|
||||
}
|
||||
})
|
||||
|
||||
if (notes.length !== noteIds.length) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Some notes not found or access denied' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get AI provider
|
||||
const config = await prisma.systemConfig.findFirst()
|
||||
const provider = getAIProvider(config || undefined)
|
||||
|
||||
// Build fusion prompt
|
||||
const notesDescriptions = notes.map((note, index) => {
|
||||
return `Note ${index + 1}: "${note.title || 'Untitled'}"
|
||||
${note.content}`
|
||||
}).join('\n\n')
|
||||
|
||||
const fusionPrompt = `You are an expert at synthesizing and merging information from multiple sources.
|
||||
|
||||
TASK: Create a unified, well-structured note by intelligently combining the following notes.
|
||||
|
||||
${prompt ? `ADDITIONAL INSTRUCTIONS: ${prompt}\n` : ''}
|
||||
|
||||
NOTES TO MERGE:
|
||||
${notesDescriptions}
|
||||
|
||||
REQUIREMENTS:
|
||||
1. Create a clear, descriptive title that captures the essence of all notes
|
||||
2. Merge and consolidate related information
|
||||
3. Remove duplicates while preserving unique details from each note
|
||||
4. Organize the content logically (use headers, bullet points, etc.)
|
||||
5. Maintain the important details and context from all notes
|
||||
6. Keep the tone and style consistent
|
||||
7. Use markdown formatting for better readability
|
||||
|
||||
Output format:
|
||||
# [Fused Title]
|
||||
|
||||
[Merged and organized content...]
|
||||
|
||||
Begin:`
|
||||
|
||||
try {
|
||||
const fusedContent = await provider.generateText(fusionPrompt)
|
||||
|
||||
return NextResponse.json({
|
||||
fusedNote: fusedContent,
|
||||
notesCount: notes.length
|
||||
})
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to generate fusion' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to process fusion request' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
92
keep-notes/app/api/ai/echo/route.ts
Normal file
92
keep-notes/app/api/ai/echo/route.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { memoryEchoService } from '@/lib/ai/services/memory-echo.service'
|
||||
|
||||
/**
|
||||
* GET /api/ai/echo
|
||||
* Fetch next Memory Echo insight for current user
|
||||
*/
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get next insight (respects frequency limits)
|
||||
const insight = await memoryEchoService.getNextInsight(session.user.id)
|
||||
|
||||
if (!insight) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
insight: null,
|
||||
message: 'No new insights available at the moment. Memory Echo will notify you when we discover connections between your notes.'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({ insight })
|
||||
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch Memory Echo insight' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/ai/echo
|
||||
* Submit feedback or mark as viewed
|
||||
*/
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await req.json()
|
||||
const { action, insightId, feedback } = body
|
||||
|
||||
if (action === 'view') {
|
||||
// Mark insight as viewed
|
||||
await memoryEchoService.markAsViewed(insightId)
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
|
||||
} else if (action === 'feedback') {
|
||||
// Submit feedback (thumbs_up or thumbs_down)
|
||||
if (!feedback || !['thumbs_up', 'thumbs_down'].includes(feedback)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid feedback. Must be thumbs_up or thumbs_down' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
await memoryEchoService.submitFeedback(insightId, feedback)
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid action. Must be "view" or "feedback"' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to process request' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,6 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Could not fetch Ollama models, using defaults:', error)
|
||||
// Garder les modèles par défaut
|
||||
}
|
||||
}
|
||||
@@ -86,7 +85,6 @@ export async function GET(request: NextRequest) {
|
||||
models: models || { tags: [], embeddings: [] }
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching models:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error.message || 'Failed to fetch models',
|
||||
|
||||
72
keep-notes/app/api/ai/notebook-summary/route.ts
Normal file
72
keep-notes/app/api/ai/notebook-summary/route.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { notebookSummaryService } from '@/lib/ai/services'
|
||||
|
||||
/**
|
||||
* POST /api/ai/notebook-summary - Generate summary for a notebook
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { notebookId } = body
|
||||
|
||||
if (!notebookId || typeof notebookId !== 'string') {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Missing required field: notebookId' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check if notebook belongs to user
|
||||
const { prisma } = await import('@/lib/prisma')
|
||||
const notebook = await prisma.notebook.findFirst({
|
||||
where: {
|
||||
id: notebookId,
|
||||
userId: session.user.id,
|
||||
},
|
||||
})
|
||||
|
||||
if (!notebook) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Notebook not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// Generate summary
|
||||
const summary = await notebookSummaryService.generateSummary(
|
||||
notebookId,
|
||||
session.user.id
|
||||
)
|
||||
|
||||
if (!summary) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: null,
|
||||
message: 'No summary available (notebook may be empty)',
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: summary,
|
||||
})
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to generate notebook summary',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
57
keep-notes/app/api/ai/reformulate/route.ts
Normal file
57
keep-notes/app/api/ai/reformulate/route.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { paragraphRefactorService } from '@/lib/ai/services/paragraph-refactor.service'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { text, option } = await request.json()
|
||||
|
||||
// Validation
|
||||
if (!text || typeof text !== 'string') {
|
||||
return NextResponse.json({ error: 'Text is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Map option to refactor mode
|
||||
const modeMap: Record<string, 'clarify' | 'shorten' | 'improveStyle'> = {
|
||||
'clarify': 'clarify',
|
||||
'shorten': 'shorten',
|
||||
'improve': 'improveStyle'
|
||||
}
|
||||
|
||||
const mode = modeMap[option]
|
||||
if (!mode) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid option. Use: clarify, shorten, or improve' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Validate word count
|
||||
const validation = paragraphRefactorService.validateWordCount(text)
|
||||
if (!validation.valid) {
|
||||
return NextResponse.json({ error: validation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
// Use the ParagraphRefactorService
|
||||
const result = await paragraphRefactorService.refactor(text, mode)
|
||||
|
||||
return NextResponse.json({
|
||||
originalText: result.original,
|
||||
reformulatedText: result.refactored,
|
||||
option: option,
|
||||
language: result.language,
|
||||
wordCountChange: result.wordCountChange
|
||||
})
|
||||
} catch (error: any) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to reformulate text' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
45
keep-notes/app/api/ai/suggest-notebook/route.ts
Normal file
45
keep-notes/app/api/ai/suggest-notebook/route.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { notebookSuggestionService } from '@/lib/ai/services/notebook-suggestion.service'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await req.json()
|
||||
const { noteContent } = body
|
||||
|
||||
if (!noteContent || typeof noteContent !== 'string') {
|
||||
return NextResponse.json({ error: 'noteContent is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Minimum content length for suggestion (20 words as per specs)
|
||||
const wordCount = noteContent.trim().split(/\s+/).length
|
||||
if (wordCount < 20) {
|
||||
return NextResponse.json({
|
||||
suggestion: null,
|
||||
reason: 'content_too_short',
|
||||
message: 'Note content too short for meaningful suggestion'
|
||||
})
|
||||
}
|
||||
|
||||
// Get suggestion from AI service
|
||||
const suggestedNotebook = await notebookSuggestionService.suggestNotebook(
|
||||
noteContent,
|
||||
session.user.id
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
suggestion: suggestedNotebook,
|
||||
confidence: suggestedNotebook ? 0.8 : 0 // Placeholder confidence score
|
||||
})
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to generate suggestion' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,53 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { auth } from '@/auth';
|
||||
import { contextualAutoTagService } from '@/lib/ai/services/contextual-auto-tag.service';
|
||||
import { getAIProvider } from '@/lib/ai/factory';
|
||||
import { getSystemConfig } from '@/lib/config';
|
||||
import { z } from 'zod';
|
||||
|
||||
const requestSchema = z.object({
|
||||
content: z.string().min(1, "Le contenu ne peut pas être vide"),
|
||||
notebookId: z.string().optional(),
|
||||
});
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { content } = requestSchema.parse(body);
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await req.json();
|
||||
const { content, notebookId } = requestSchema.parse(body);
|
||||
|
||||
// If notebookId is provided, use contextual suggestions (IA2)
|
||||
if (notebookId) {
|
||||
const suggestions = await contextualAutoTagService.suggestLabels(
|
||||
content,
|
||||
notebookId,
|
||||
session.user.id
|
||||
);
|
||||
|
||||
// Convert label → tag to match TagSuggestion interface
|
||||
const convertedTags = suggestions.map(s => ({
|
||||
tag: s.label, // Convert label to tag
|
||||
confidence: s.confidence,
|
||||
// Keep additional properties for client-side use
|
||||
...(s.reasoning && { reasoning: s.reasoning }),
|
||||
...(s.isNewLabel !== undefined && { isNewLabel: s.isNewLabel })
|
||||
}));
|
||||
|
||||
return NextResponse.json({ tags: convertedTags });
|
||||
}
|
||||
|
||||
// Otherwise, use legacy auto-tagging (generates new tags)
|
||||
const config = await getSystemConfig();
|
||||
const provider = getAIProvider(config);
|
||||
const tags = await provider.generateTags(content);
|
||||
|
||||
return NextResponse.json({ tags });
|
||||
} catch (error: any) {
|
||||
console.error('Erreur API tags:', error);
|
||||
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json({ error: error.issues }, { status: 400 });
|
||||
}
|
||||
|
||||
@@ -71,7 +71,6 @@ export async function POST(request: NextRequest) {
|
||||
details
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('AI embeddings test error:', error)
|
||||
const config = await getSystemConfig()
|
||||
const providerType = config.AI_PROVIDER_EMBEDDING || 'ollama'
|
||||
const details = getProviderDetails(config, providerType)
|
||||
|
||||
@@ -33,7 +33,6 @@ export async function POST(request: NextRequest) {
|
||||
responseTime: endTime - startTime
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('AI tags test error:', error)
|
||||
const config = await getSystemConfig()
|
||||
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -72,7 +72,6 @@ export async function GET(request: NextRequest) {
|
||||
details
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('AI test error:', error)
|
||||
const config = await getSystemConfig()
|
||||
const providerType = config.AI_PROVIDER_EMBEDDING || 'ollama'
|
||||
const details = getProviderDetails(config, providerType)
|
||||
|
||||
99
keep-notes/app/api/ai/title-suggestions/route.ts
Normal file
99
keep-notes/app/api/ai/title-suggestions/route.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getAIProvider } from '@/lib/ai/factory'
|
||||
import { getSystemConfig } from '@/lib/config'
|
||||
import { z } from 'zod'
|
||||
|
||||
const requestSchema = z.object({
|
||||
content: z.string().min(1, "Le contenu ne peut pas être vide"),
|
||||
})
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json()
|
||||
const { content } = requestSchema.parse(body)
|
||||
|
||||
// Vérifier qu'il y a au moins 10 mots
|
||||
const wordCount = content.split(/\s+/).length
|
||||
|
||||
if (wordCount < 10) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Le contenu doit avoir au moins 10 mots' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const config = await getSystemConfig()
|
||||
const provider = getAIProvider(config)
|
||||
|
||||
// Détecter la langue du contenu (simple détection basée sur les caractères)
|
||||
const hasNonLatinChars = /[\u0400-\u04FF\u0600-\u06FF\u4E00-\u9FFF\u0E00-\u0E7F]/.test(content)
|
||||
const isPersian = /[\u0600-\u06FF]/.test(content)
|
||||
const isChinese = /[\u4E00-\u9FFF]/.test(content)
|
||||
const isRussian = /[\u0400-\u04FF]/.test(content)
|
||||
const isArabic = /[\u0600-\u06FF]/.test(content)
|
||||
|
||||
// Déterminer la langue du prompt système
|
||||
let promptLanguage = 'en'
|
||||
let responseLanguage = 'English'
|
||||
|
||||
if (isPersian) {
|
||||
promptLanguage = 'fa' // Persan
|
||||
responseLanguage = 'Persian'
|
||||
} else if (isChinese) {
|
||||
promptLanguage = 'zh' // Chinois
|
||||
responseLanguage = 'Chinese'
|
||||
} else if (isRussian) {
|
||||
promptLanguage = 'ru' // Russe
|
||||
responseLanguage = 'Russian'
|
||||
} else if (isArabic) {
|
||||
promptLanguage = 'ar' // Arabe
|
||||
responseLanguage = 'Arabic'
|
||||
}
|
||||
|
||||
// Générer des titres appropriés basés sur le contenu
|
||||
const titlePrompt = promptLanguage === 'en'
|
||||
? `You are a title generator. Generate 3 concise, descriptive titles for the following content.
|
||||
|
||||
IMPORTANT INSTRUCTIONS:
|
||||
- Use ONLY the content provided below between the CONTENT_START and CONTENT_END markers
|
||||
- Do NOT use any external knowledge or training data
|
||||
- Focus on the main topics and themes in THIS SPECIFIC content
|
||||
- Be specific to what is actually discussed
|
||||
|
||||
CONTENT_START: ${content.substring(0, 500)} CONTENT_END
|
||||
|
||||
Respond ONLY with a JSON array: [{"title": "title1", "confidence": 0.95}, {"title": "title2", "confidence": 0.85}, {"title": "title3", "confidence": 0.75}]`
|
||||
: `Tu es un générateur de titres. Génère 3 titres concis et descriptifs pour le contenu suivant en ${responseLanguage}.
|
||||
|
||||
INSTRUCTIONS IMPORTANTES :
|
||||
- Utilise SEULEMENT le contenu fourni entre les marqueurs CONTENT_START et CONTENT_END
|
||||
- N'utilise AUCUNE connaissance externe ou données d'entraînement
|
||||
- Concentre-toi sur les sujets principaux et thèmes de CE CONTENU SPÉCIFIQUE
|
||||
- Sois spécifique à ce qui est réellement discuté
|
||||
|
||||
CONTENT_START: ${content.substring(0, 500)} CONTENT_END
|
||||
|
||||
Réponds SEULEMENT avec un tableau JSON: [{"title": "titre1", "confidence": 0.95}, {"title": "titre2", "confidence": 0.85}, {"title": "titre3", "confidence": 0.75}]`
|
||||
|
||||
const titles = await provider.generateTitles(titlePrompt)
|
||||
|
||||
// Créer les suggestions
|
||||
const suggestions = titles.map((t: any) => ({
|
||||
title: t.title,
|
||||
confidence: Math.round(t.confidence * 100),
|
||||
reasoning: `Basé sur le contenu`
|
||||
}))
|
||||
|
||||
return NextResponse.json({ suggestions })
|
||||
} catch (error: any) {
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json({ error: error.issues }, { status: 400 })
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Erreur lors de la génération des titres' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
90
keep-notes/app/api/ai/transform-markdown/route.ts
Normal file
90
keep-notes/app/api/ai/transform-markdown/route.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { getAIProvider } from '@/lib/ai/factory'
|
||||
import { getSystemConfig } from '@/lib/config'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { text } = await request.json()
|
||||
|
||||
// Validation
|
||||
if (!text || typeof text !== 'string') {
|
||||
return NextResponse.json({ error: 'Text is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Validate word count
|
||||
const wordCount = text.split(/\s+/).length
|
||||
if (wordCount < 10) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Text must have at least 10 words to transform' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (wordCount > 500) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Text must have maximum 500 words to transform' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const config = await getSystemConfig()
|
||||
const provider = getAIProvider(config)
|
||||
|
||||
// Detect language from text
|
||||
const hasFrench = /[àâäéèêëïîôùûüÿç]/i.test(text)
|
||||
const responseLanguage = hasFrench ? 'French' : 'English'
|
||||
|
||||
// Build prompt to transform text to Markdown
|
||||
const prompt = hasFrench
|
||||
? `Tu es un expert en Markdown. Transforme ce texte ${responseLanguage} en Markdown bien formaté.
|
||||
|
||||
IMPORTANT :
|
||||
- Ajoute des titres avec ## pour les sections principales
|
||||
- Utilise des listes à puces (-) ou numérotées (1.) quand approprié
|
||||
- Ajoute de l'emphase (gras **texte**, italique *texte*) pour les mots clés
|
||||
- Utilise des blocs de code pour le code ou les commandes
|
||||
- Présente l'information de manière claire et structurée
|
||||
- GARDE le même sens et le contenu, seul le format change
|
||||
|
||||
Texte à transformer :
|
||||
${text}
|
||||
|
||||
Réponds SEULEMENT avec le texte transformé en Markdown, sans explications.`
|
||||
: `You are a Markdown expert. Transform this ${responseLanguage} text into well-formatted Markdown.
|
||||
|
||||
IMPORTANT:
|
||||
- Add headings with ## for main sections
|
||||
- Use bullet lists (-) or numbered lists (1.) when appropriate
|
||||
- Add emphasis (bold **text**, italic *text*) for key terms
|
||||
- Use code blocks for code or commands
|
||||
- Present information clearly and structured
|
||||
- KEEP the same meaning and content, only change the format
|
||||
|
||||
Text to transform:
|
||||
${text}
|
||||
|
||||
Respond ONLY with the transformed Markdown text, no explanations.`
|
||||
|
||||
|
||||
const transformedText = await provider.generateText(prompt)
|
||||
|
||||
|
||||
return NextResponse.json({
|
||||
originalText: text,
|
||||
transformedText: transformedText,
|
||||
language: responseLanguage
|
||||
})
|
||||
} catch (error: any) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to transform text to Markdown' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user